Реагировать — Функция, не знающая о состоянии при вызове из события keydown

#javascript #reactjs #use-state

Вопрос:

Я работал над изучением React и при этом создал приложение для выполнения задач с использованием компонентов класса. Недавно я работал над созданием копии приложения todo, используя функции и крючки вместо классов.

После рефакторинга кода все, похоже, работает правильно, за исключением одного варианта использования.

СЛУЧАЙ 1: При вводе ввода и нажатии кнопки «Добавить» для вызова addItem() нового элемента задачи добавляется, как и ожидалось.

СЛУЧАЙ 2: При вводе ввода и нажатии клавиши enter для запуска обработчика событий, который вызывает addItem() значение newItem , всегда совпадает с его начальным значением.

Я ни за что на свете не могу понять, почему addItem() при вызове по-другому ведет себя при нажатии кнопки «Добавить» по сравнению с нажатием клавиши «Ввод».

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)}>amp;#10006;</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
};
 

Ответ №1:

Это происходит потому, что вы добавляете прослушиватель событий внутри useEffect , и в это время значение newItem является вашим начальным новым элементом.

Чтобы это сработало , вы можете добавить newItem в массив dependecy для обновления прослушивателей событий при каждом обновлении newItem.

 useEffect(() => {
        document.addEventListener('keydown', handleKeyDown);
        return () => document.removeEventListener('keydown', handleKeyDown);
    }, [newItem]);
 

Однако это всего лишь решение, но не рекомендуемое решение. Добавление прослушивателей событий таким образом не очень похоже на реакцию.

Вместо записи прослушивателей событий в useEffect.Вместо этого вы должны связать событие, подобное этому

 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>
    )
}
 

И не забудьте добавить его в родительский компонент

 <TodoForm
                newItem={newItem}
                handleChange={handleChange}
                handleKeydown={handleKeydown}//this is new
                addItem={addItem}
            />
 

Комментарии:

1. Спасибо за подробный ответ! Я ценю решение моей текущей проблемы и рекомендуемое решение для React. Я реализовал onKeyDown это, и все работает так, как ожидалось.

Ответ №2:

попробуйте добавить это в свой ввод:

 <input type='text' value={newItem} onChange={handleChange} onKeyDown={(event) => {
          if (event.key === "Enter") {
            addItem();
          }
        }}/>
 

И удалите список событий из вашего useEffect() — на самом деле вы можете удалить весь useEffect (), так как он пытается обработать только ваш список событий…

Ответ №3:

Это классический случай несвежего закрытия. Вы добавляете событие keydown в документ только один раз, когда компонент монтируется, и он запоминает функцию только при первом рендеринге, когда состояние является строкой по умолчанию.

Один из способов решить эту проблему-добавить newItem в качестве зависимости к вашему useEffect , вы также можете использовать onKeyDown опору для ввода вашего нового задания, но это может действовать не так, как вы ожидаете (например, ввод должен быть сфокусирован для запуска события).

Комментарии:

1. Спасибо! В этом действительно была проблема. Я поменял его местами, чтобы использовать onKeyDown