#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