React Todo应用:点击更新按钮仅替换对应项为表单组件的问题
问题分析
你当前的问题核心在于全局布尔状态isClicked控制了所有待办项的渲染切换:点击任意更新按钮后,这个全局状态变为true,所有待办项都会判断ctx.isClicked并切换为表单组件,而非仅目标项。
解决方案
需要把全局布尔状态改为记录当前编辑的待办项ID,这样就能精准控制单个项的渲染切换。
1. 修改Context Provider
将原有的isClicked状态替换为editTodoId,用于存储当前正在编辑的待办项ID:
export const TodoContext = React.createContext<TodoContextObj | null>({ todos: [], edit: { id: '', value: '' }, editTodoId: null, // 替换原isClicked addTodo: (todo: Todo) => {}, removeTodo: (id: string) => {}, completeTodo: (id: string) => {}, updateTodo: (id: string, todo: Todo) => {}, deleteTodos: (key: string) => {}, setEditTodoId: (id: string | null) => {}, // 替换原setClicked }); type Props = { children: React.ReactNode }; const initialValue: InitialState = { id: null, value: '' }; const ContextProvider: React.FC<Props> = props => { const [todos, setTodos] = useState<Array<Todo>>([]); const [edit, setEdit] = useState(initialValue); const [editTodoId, setEditTodoId] = useState<string | null>(null); // 替换原isClicked状态 useEffect(() => { const data = localStorage.getItem('todos'); if (data) setTodos(JSON.parse(data)); }, []); // 替换原setClicked函数 const setEditTodoIdHandler = (id: string | null) => setEditTodoId(id); const addTodo = (todo: Todo) => { if (!todo.text || /^\s*$/.test(todo.text)) return; const newTodos = [...todos, todo]; setTodos(newTodos); setEditTodoId(null); // 新增:添加后退出编辑状态 localStorage.setItem('todos', JSON.stringify(newTodos)); }; const updateTodo = (id: string, value: Todo) => { if (!value.text || /^\s*$/.test(value.text)) return; if (edit.id) setEdit({ id: value.id, value: value.text }); // 用prev状态更新,避免闭包问题 setTodos(prev => { const updated = prev.map(item => (item.id === id ? value : item)); localStorage.setItem('todos', JSON.stringify(updated)); return updated; }); setEditTodoId(null); // 更新后退出编辑状态 }; // removeTodo、completeTodo、deleteTodos函数保持不变 const contextValue: TodoContextObj = { todos, editTodoId, // 替换原isClicked addTodo, removeTodo, completeTodo, updateTodo, deleteTodos, setEditTodoId: setEditTodoIdHandler, // 替换原setClicked }; return ( <TodoContext.Provider value={contextValue}> {props.children} </TodoContext.Provider> ); }; export default ContextProvider;
2. 修改Todos组件
更新渲染逻辑,仅当当前待办项ID等于editTodoId时才显示表单,并传递待办项数据给表单:
const Todos: React.FC<Props> = props => { const ctx = useContext(TodoContext); const editTodo = (value: Todo) => { ctx?.setEditTodoId(value.id); // 设置当前编辑的ID // 可选:把当前todo数据存入edit状态,方便表单初始化 ctx?.setEdit({ id: value.id, value: value.text }); }; const completeTodo = (todo: string) => ctx?.completeTodo(todo); const removeTodo = (todo: string) => ctx?.removeTodo(todo); return ( <div> <AnimatePresence> {ctx?.todos.map((todo) => { // 用todo.id作为key,避免index带来的渲染问题 return ( <motion.div key={todo.id} whileInView={{ x: [-100, 0], opacity: [0, 1] }} exit={{ x: [0, 20], opacity: [1, 0] }} transition={{ duration: 1, ease: 'easeInOut', delayChildren: 0.5, }} className={`${ todo.isComplete ? 'todo-row complete' : 'todo-row' } app__flex`} > {/* 判断当前项是否为编辑状态 */} {ctx?.editTodoId === todo.id ? ( <TodoUpdateForm todo={todo} /> // 传递当前todo给表单 ) : ( <div className="todos-item"> <div onClick={() => completeTodo(todo.id)} className="app__todo-list-item" > {todo.text} </div> <motion.div whileInView={{ opacity: [0, 1] }} transition={{ duration: 2, ease: 'easeInOut', delayChildren: 0.5, }} className="icons app__flex" > <RiCloseCircleLine className="delete-icon" onClick={() => removeTodo(todo.id)} /> <TiEdit className="edit-icon" onClick={() => editTodo(todo)} /> </motion.div> </div> )} </motion.div> ); })} </AnimatePresence> </div> ); }; export default Todos;
3. 补充TodoUpdateForm逻辑(可选)
确保表单能接收todo属性,并在提交更新后退出编辑状态:
const TodoUpdateForm: React.FC<{ todo: Todo }> = ({ todo }) => { const ctx = useContext(TodoContext); const [inputValue, setInputValue] = useState(todo.text); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!inputValue.trim()) return; ctx?.updateTodo(todo.id, { ...todo, text: inputValue }); }; return ( <form onSubmit={handleSubmit} className="update-form"> <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <button type="submit">保存</button> <button type="button" onClick={() => ctx?.setEditTodoId(null)}>取消</button> </form> ); }; export default TodoUpdateForm;
关键注意点
- 替换
isClicked为editTodoId,实现精准的单一项控制; - 用
todo.id作为列表项的key,避免使用index导致的渲染异常; - 更新
updateTodo时使用prev状态,解决闭包导致的localStorage数据不一致问题; - 表单提交或取消时,重置
editTodoId为null,退出编辑状态。
内容的提问来源于stack exchange,提问作者Jesse
相关产品推荐
相关产品推荐

