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

动态添加数组对象时useState导致React应用异常

问题根源与解决方案

你的问题出在两个关键地方:缺少唯一的key属性,以及组件内部state没有和props同步,下面一步步拆解:

1. 为什么添加新元素后会出现异常?

(1)React列表渲染缺少key导致组件复用混乱

在父组件的map渲染中,你没有给每个<ExpenseItem>添加唯一的key属性:

// 原父组件代码:没有key
{props.myExpenses.map((expense) => (<ExpenseItem title={expense.title} amount={expense.amount} date={expense.date} ></ExpenseItem>))}

React默认会用数组索引作为key,当你向数组中添加新元素(尤其是非尾部位置),React会错误地复用已有组件实例,而不是创建新的ExpenseItem。这就导致新增的组件会继承旧组件的state值(比如之前修改过的title: 'Updated!'),同时原有组件的state也会因为复用逻辑混乱,出现显示错误。

(2)组件state初始化后未与props同步

你的ExpenseItem组件用useState(props.title)初始化title state,但这个初始化只在组件第一次挂载时执行一次。当父组件传入新的props.title(比如新增的支出项),如果组件被复用,useState不会重新初始化,state会保持原来的旧值,导致显示的title和传入的props.title不一致。


2. 完整解决方案

第一步:给列表项添加唯一key

修改父组件的map渲染,用每个支出项的唯一id作为key(这是React列表渲染的最佳实践):

{props.myExpenses.map((expense) => (
  <ExpenseItem 
    key={expense.id} // 用唯一的id作为key,避免组件复用错误
    title={expense.title} 
    amount={expense.amount} 
    date={expense.date} 
  ></ExpenseItem>
))}

第二步:修复state与props的同步问题

根据你的需求,有两种方案可选:

方案A:仅在组件内部修改title(不需要同步回父数组)

如果修改title只是组件内部的临时状态,不需要更新父组件的数据源,可以用useEffect监听props.title的变化,自动同步到state:

function ExpenseItem(props) { 
  const [title, setTitle] = useState(props.title); 

  // 当props.title变化时,更新state
  useEffect(() => {
    setTitle(props.title);
  }, [props.title]); // 依赖项:props.title

  const clickHandler = () => { 
    setTitle('Updated!'); 
  }; 

  return ( 
    <Card className="expense-item"> 
      <ExpenseDate date={props.date}></ExpenseDate> 
      <div className="expense-item__description"> 
        <h2>{title}</h2> 
      </div> 
      <div className="expense-item__price"> 
        {props.amount} 
      </div> 
      <button onClick={clickHandler}>Change Title</button> 
    </Card> 
  ); 
}

方案B:把title状态提升到父组件(推荐,符合React单向数据流)

如果希望修改后的title能保存到父组件的数据源中,应该把状态提升到父组件,让子组件变成受控组件:

父组件修改:

// 假设父组件持有myExpenses的状态
function ParentComponent() {
  const [myExpenses, setMyExpenses] = useState(/* 初始支出数据 */);

  // 定义修改title的函数,传递给子组件
  const handleUpdateTitle = (expenseId, newTitle) => {
    setMyExpenses(prevExpenses => 
      prevExpenses.map(expense => 
        expense.id === expenseId ? {...expense, title: newTitle} : expense
      )
    );
  };

  return ( 
    <div> 
      <Card className="expenses"> 
        <ExpensesFilter selected={filteredYear} onFilterChanged={filterChangedHandler} /> 
        {myExpenses.map((expense) => (
          <ExpenseItem 
            key={expense.id}
            id={expense.id} // 传递id给子组件,用于定位要修改的项
            title={expense.title} 
            amount={expense.amount} 
            date={expense.date} 
            onUpdateTitle={handleUpdateTitle} // 传递修改函数
          ></ExpenseItem>
        ))} 
      </Card> 
    </div> 
  );
}

子组件修改:

function ExpenseItem(props) { 
  const clickHandler = () => { 
    // 调用父组件传递的函数修改title
    props.onUpdateTitle(props.id, 'Updated!'); 
  }; 

  return ( 
    <Card className="expense-item"> 
      <ExpenseDate date={props.date}></ExpenseDate> 
      <div className="expense-item__description"> 
        <h2>{props.title}</h2> 
      </div> 
      <div className="expense-item__price"> 
        {props.amount} 
      </div> 
      <button onClick={clickHandler}>Change Title</button> 
    </Card> 
  ); 
}

内容的提问来源于stack exchange,提问作者Dani

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.01 02:12:28