I’ve been working on learning React, and in doing so built a todo app using class components. Recently I have been working to make a copy of the todo app using functions and hooks instead of classes.
Having refactored the code everything appears to be working correctly aside from one use case.
CASE 1:
When typing into the input and clicking “Add” button to call addItem()
the new todo item adds as expected.
CASE 2:
When typing into the input and hitting enter to trigger an event handler which calls addItem()
the value of newItem
is always the same as its initial value.
I can’t for the life of me figure out why addItem()
behaves differently when called from the click of the “Add” button versus the press of the “Enter” key.
TodoAppContainer.js
import React, { useState, useEffect } from 'react'; import TodoList from './TodoList'; import TodoForm from './TodoForm'; import { GenerateID } from './generateId'; export default function TodoListContainer(props) { const [newItem, setNewItem] = useState('New Todo'); const [items, setItems] = useState([{ name: 'Build Todo List App', done: true, key: GenerateID.next().value }]); const handleKeyDown = e => { if (e.key === 'Enter') addItem(); }; const handleChange = ({ target }) => { console.log("handleChange"); // capture text from input field const text = target.value; // update state value for "newItem" setNewItem(text); }; const addItem = () => { console.log("addItem"); // exit early if there is no item if (!!!newItem.trim()) return; // build new item to add const itemToAdd = { name: newItem, done: false, key: GenerateID.next().value }; // update state with new item setItems(prevItems => [itemToAdd, ...prevItems]); // clear text for new item setNewItem(''); }; const completeItem = key => { console.log('completeItem'); // create new copy of state items const updatedItems = [...items]; // get the index of the item to update const index = updatedItems.findIndex(v => v.key === key); // toggle the done state of the item updatedItems[index].done = !updatedItems[index].done; // update the state setItems(updatedItems); }; const removeItem = key => { console.log('removeItem'); // create copy of filtered items const filteredItems = items.filter(v => v.key !== key); // update the state of items setItems(filteredItems); } // get count of items that are "done" const getTodoCount = () => items.filter(v => v.done).length; useEffect(() => { document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, []); return ( <section className='todo-section'> <TodoForm newItem={newItem} handleChange={handleChange} addItem={addItem} /> <TodoList items={items} count={getTodoCount()} onClick={completeItem} onRemove={removeItem} /> </section> ); }
TodoForm.js
import React from 'react'; import PropTypes from 'prop-types'; export default function TodoForm(props) { const { newItem, handleChange, addItem } = props; return ( <div className='todo-form'> <input type='text' value={newItem} onChange={handleChange} /> <button onClick={addItem}>Add</button> </div> ) } TodoForm.propTypes = { newItem: PropTypes.string.isRequired, addItem: PropTypes.func.isRequired, handleChange: PropTypes.func.isRequired };
TodoList.js
import React from 'react'; import PropTypes from 'prop-types'; export default function TodoList(props) { const { items, count, onClick, onRemove } = props; const shrug = '¯\_(ツ)_/¯'; const shrugStyles = { fontSize: '2rem', fontWeight: 400, textAlign: 'center' }; const buildItemHTML = ({ key, name, done }) => { const className = done ? 'todo-item done' : 'todo-item'; return ( <li className={className} key={key}> <span className='item-name' onClick={() => onClick(key)}>{name}</span> <span className='remove-icon' onClick={() => onRemove(key)}>✖</span> </li> ); }; return ( <div> <p style={{ margin: 0, padding: '0.75em' }}>{count} of {items.length} Items Complete!</p> <ul className='todo-list'> {items.length ? items.map(buildItemHTML) : <h1 style={shrugStyles}>{shrug}<br />No items here...</h1>} </ul> </div> ); }; TodoList.propTypes = { count: PropTypes.number.isRequired, items: PropTypes.array.isRequired, onClick: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired };
Advertisement
Answer
This is happening because the you are adding the event listener inside useEffect
and at that time the value of newItem
is your initial newItem.
To make it work , you can add newItem to dependecy array to update the event listeners, every time newItem updates.
useEffect(() => { document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [newItem]);
This however is just a solution but not the recommended solution. Adding event listeners this way is not very React-ish.
Instead of writing event listeners in useEffect.You should instead bind an event like this
export default function TodoForm(props) { const { newItem, handleChange, addItem ,handleKeyDown} = props; return ( <div className='todo-form'> <input type='text' value={newItem} onChange={handleChange} onKeyDown={handleKeyDown}// this is new /> <button onClick={addItem}>Add</button> </div> ) }
And don’t forget to add it in the parent component
<TodoForm newItem={newItem} handleChange={handleChange} handleKeydown={handleKeydown}//this is new addItem={addItem} />