You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 12:03:30