React Todo应用useEffect重复执行及二次添加项崩溃问题排查
React Todo应用问题排查与修复
应用功能概述
- 输入事项后按回车添加至列表
- 事项暂存于localStorage
- 支持标签解析(如输入
@tom:buy milk时,标签为tom,内容为buy milk)
问题分析与修复
1. 应用加载时useEffect执行两次
原因:React 18开发模式下,StrictMode会强制组件挂载两次,用于检测未清理的副作用或非幂等操作,这是开发环境专属特性,生产环境不会触发。
修复方案:
如果想避免开发环境下重复执行localStorage写入和日志打印,可通过useRef标记首次渲染:
// 在App组件中添加 const isFirstRender = useRef(true); useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; return; } localStorage.setItem("toDos", JSON.stringify(dataLoaded)) console.log('update') }, [dataLoaded])
若不介意开发环境的重复执行,也可忽略该问题,生产环境会自动恢复正常。
2. 保存第二个事项时应用崩溃
根本原因:
processInput函数处理空输入或不符合正则的内容时会返回undefined,将其加入items数组后,TabItem组件尝试访问undefined的text、tags等属性,直接抛出错误。- 直接修改DOM元素的
value属性不符合React受控组件设计规范,易导致状态不一致。
修复方案:
修改InputBar组件为受控组件,并确保processInput始终返回有效对象:
import { useState } from 'react' import { nanoid } from 'nanoid' import '../../assets/style.css'; export default function InputBar(props){ const [inputValue, setInputValue] = useState(''); const today = new Date().toDateString(); function processInput(s) { const trimmedText = s.trim(); // 过滤空输入 if (!trimmedText) return null; const matchResult = trimmedText.match(/^(@.+?:)?(.+)/); if (matchResult) { return { tags: matchResult[1] ? matchResult[1].slice(1, -1).split('@') : ['default'], text: matchResult[2].trim(), created: today, id: nanoid() } } // 兜底返回有效结构,避免undefined return { tags: ['default'], text: trimmedText, created: today, id: nanoid() } } function handleKeyDown(e) { if(e.key === 'Enter'){ const newTodoItem = processInput(inputValue); // 仅添加有效事项 if (newTodoItem) { props.setNewList(oldData => ({ ...oldData, items: [newTodoItem, ...oldData.items] })); // 受控方式清空输入 setInputValue(''); } } } return( <div className="main-input-div"> <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} /> </div> ) }
关键修改点:
- 改用受控组件管理输入值,避免直接操作DOM
- 给
processInput增加空输入过滤和兜底逻辑,确保返回值为null或有效对象 - 使用
e.key === 'Enter'替代已废弃的keyCode属性 - 添加条件判断,仅当事项有效时才添加到列表
修改后的核心代码
App组件
import { useState, useEffect, useRef } from 'react' import './assets/style.css'; import data from '../data/data.json' import InputBar from "./components/InputBar/InputBar" import NavBar from "./components/NavBar/NavBar" import TabItem from "./components/Tab/TabItem" function App() { const [dataLoaded, setDataLoaded] = useState( () => JSON.parse(localStorage.getItem("toDos")) || data ) const isFirstRender = useRef(true); useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; return; } localStorage.setItem("toDos", JSON.stringify(dataLoaded)) console.log('update') }, [dataLoaded]) function deleteItem(id){ setDataLoaded(oldData=>{ return { ...oldData, "items":oldData.items.filter(el => el.id !== id) } }) } return ( <div className='container'> <NavBar/> <InputBar setNewList={setDataLoaded} /> {dataLoaded.items.map(el => ( <TabItem item={el} key={el.id} delete={deleteItem}/> ))} </div> ) } export default App
TabItem组件
无需修改,保持原代码即可。
内容的提问来源于stack exchange,提问作者giaggi
相关产品推荐
相关产品推荐

