Skip to content
Advertisement

React – Function unaware of state when called from keydown event

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}
            />
User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement