动态添加数组对象时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
相关产品推荐
相关产品推荐

