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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 07:50:20