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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 16:24:54