React回调闭包获取最新状态:动态输入列表删除异常修复
问题描述
我在开发一个带动态表单输入列表的页面,点击按钮可以添加/删除输入字段。父组件<Ingredients />维护输入项状态,存的是<Ingredient />子组件的列表。每个子组件的删除按钮通过DeleteInput回调通知父组件删除对应项。
现在的问题是:点击删除按钮时,目标项之后的所有字段都会消失。排查发现回调触发时,DeleteInput里的inputList是组件首次创建时的旧值,因为回调被包在子组件的闭包里,即便定义在父组件也没用。比如添加3个输入框后删除第2个,回调里只能看到第一个输入项的状态。
我试过用useCallback并把inputList作为依赖,但问题依旧。
原代码如下:
// Parent export function Ingredients() { const [inputList, setInputList] = useState([]); const [key, setKey] = useState(1); function AddInput() { setInputList(inputList.concat([<Ingredient key={key} index={key} deleteInput={deleteInput} />])) setKey(key+1); } // Approach 1: Using a normal regular function function DeleteInput() { console.log("Inside"); console.log(inputList); let tmp = inputList; tmp.splice(indexToDel, 1) setInputList(tmp); } // Approach 2: Using a useCallback hook const DeleteInput = useCallback((indexToDel) => { console.log("Inside"); console.log(inputList); let tmp = inputList; tmp.splice(indexToDel, 1) setInputList(tmp); }, [inputList]) console.log("Outside"); console.log(inputList); return ( <div> {inputList} <button type='button' onClick={AddInput}>Add More</button> </div> ) } // Child function Ingredient({index, deleteInput}) { const i = index; return ( <div className={styles.ingredient}> <input className={styles.ingredientname} type='text' placeholder='Ingredient'></input> <button type='button' onClick={() => DeleteInput(index)}>X</button> </div> ) }
添加3个输入框并删除第2个时的打印输出:
修复方案
核心问题有三个:状态存储React元素而非数据、直接修改原数组导致状态不更新、闭包捕获旧状态,按以下步骤修复:
把状态从组件列表改成数据列表
不要在inputList里存<Ingredient>组件,而是存每个输入项的数据(比如带唯一ID的对象),渲染时再通过map生成组件。这样状态更新时,所有子组件都会拿到最新的回调。使用函数式更新获取最新状态
调用setInputList时用函数式写法,直接拿到最新的状态快照,避免闭包捕获旧值。同时不要直接修改原数组(splice是破坏性操作),要返回新数组。修复子组件的回调拼写错误
子组件里的DeleteInput要改成props传入的deleteInput(小写d),否则会调用未定义的函数。
修复后的完整代码:
// Parent import { useState } from 'react'; import Ingredient from './Ingredient'; export function Ingredients() { // 存储输入项数据,每个项带唯一id const [inputList, setInputList] = useState([]); const [nextId, setNextId] = useState(1); function AddInput() { // 添加新的数据项,而非组件 setInputList(prev => [...prev, { id: nextId, value: '' }]); setNextId(prev => prev + 1); } // 用函数式更新获取最新状态,返回新数组 const deleteInput = (idToDelete) => { setInputList(prev => prev.filter(item => item.id !== idToDelete)); }; return ( <div> {/* 遍历数据生成组件,每个组件传唯一id和删除回调 */} {inputList.map(item => ( <Ingredient key={item.id} id={item.id} deleteInput={deleteInput} /> ))} <button type='button' onClick={AddInput}>Add More</button> </div> ) } // Child function Ingredient({ id, deleteInput }) { return ( <div className={styles.ingredient}> <input className={styles.ingredientname} type='text' placeholder='Ingredient' /> {/* 点击时传入当前项的id */} <button type='button' onClick={() => deleteInput(id)}>X</button> </div> ) } export default Ingredient;
额外说明:
- 用
id而非index作为删除标识,避免删除后后续项的index变化导致的错误。 - 函数式更新
setInputList(prev => ...)确保每次都能拿到最新的状态,彻底解决闭包问题。 - 用
filter返回新数组,替代slice+splice的破坏性操作,符合React状态不可变的原则。
内容的提问来源于stack exchange,提问作者Arron
相关产品推荐
相关产品推荐

