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

React列表删除项后输入状态异常:状态未保留问题求助

问题:列表删除项后输入值状态异常

我想实现一个带添加按钮和每项删除按钮的列表,以下是基础代码(项目中逻辑一致但更复杂):

App.tsx

import React, { createRef, useEffect, useState } from 'react';
import ListItem from "./list_item";

const testData = [
    {
        value: 1,
    }
]
type EnhancedListItem = {
    id: number;
    ref: React.RefObject<any>;
    item: React.ReactElement;
}

function App() {

    const [enhancedListItems, setEnhancedListItems] = useState<EnhancedListItem[]>([]);

    useEffect(() => {
        const newEnhancedListItems: EnhancedListItem[] = [];
        testData.forEach((t, i) => {
            const ref = createRef<any>();
            const item = React.cloneElement(<ListItem />, { id: i, defaultValue: t.value, ref });
            newEnhancedListItems.push({ id: i, ref, item });
        });
        setEnhancedListItems(newEnhancedListItems);
    }, []);

    function onAddListItemButtonClick(): void {
        const ref = createRef<any>();
        const newId = enhancedListItems.reduce((p: number, c) => Math.max(p, c.id), 0) + 1;
        const newObject = { defaultValue: Math.floor(Math.random() * 10), id: newId }
        const item = React.cloneElement(<ListItem />, { ...newObject, ref });
        setEnhancedListItems([...enhancedListItems, { id: newId, item, ref }]);
    }

    function onRemoveButtonClick(id: number): void {
        setEnhancedListItems(enhancedListItems.filter(t => t.id !== id));
    }

    return (
        <div>
            {enhancedListItems.map((t, i) => (
                <div key={i}>
                    <span>{t.item}</span>
                    <span><button onClick={() => onRemoveButtonClick(t.id)}>-</button></span>
                </div>
            ))}
            <button onClick={onAddListItemButtonClick}>+</button>
        </div>
    );
}

export default App;

ListItem.tsx

import React, { useState } from "react";

export type ListItemProps = {
    id?: number;
    defaultValue?: string;
}

function ListItem(props: ListItemProps, ref: React.ForwardedRef<any>): React.ReactElement {

    const [value, setValue] = useState(props.defaultValue ?? "");

    function onInputChange(e: React.ChangeEvent<HTMLInputElement>): void {
        setValue(e.target.value);
    }

    return (
        <div>
            <span>{props.id}</span>
            <input type="text" value={value} onChange={onInputChange} />
        </div>
    );
}

export default React.forwardRef(ListItem);

问题描述

点击某一项的删除按钮后,该项ID消失,但其他项的输入值会错乱,像是最后一项被移除,所有项的值变成前一项的值。请问为何输入值状态未被保留?是否是cloneElement流程有问题?

更新:简化复现代码

import React, { useState } from 'react';

type EnhancedListItem = {
    id: number;
}

function App() {

    const [enhancedListItems, setEnhancedListItems] = useState<EnhancedListItem[]>([]);

    function onAddListItemButtonClick(): void {
        const newId = enhancedListItems.reduce((p: number, c) => Math.max(p, c.id), 0) + 1;
        setEnhancedListItems([...enhancedListItems, { id: newId }]);
    }

    function onRemoveButtonClick(id: number): void {
        setEnhancedListItems(enhancedListItems.filter(t => t.id !== id));
    }

    return (
        <div>
            {enhancedListItems.map((t, i) => (
                <div key={i}>
                    <span>{t.id}</span>
                    <input type="text" />
                    <button onClick={() => onRemoveButtonClick(t.id)}>-</button>
                </div>
            ))}
            <button onClick={onAddListItemButtonClick}>+</button>
        </div>
    );
}

export default App;

解决方案

核心原因:错误使用数组索引作为key

React的Diff算法依赖key识别列表元素的唯一性。你当前用数组索引key={i},删除中间项后,剩余项的索引会重新排列,React会误以为新列表的索引对应原列表后续位置的组件,从而复用错误的组件实例,导致输入值错位。

另外,原代码把React元素(t.item)存在状态里是反模式,React状态应该存储数据而非渲染结果,这会导致状态更新时组件复用逻辑混乱。

修复步骤

1. 用唯一ID替换索引作为key

渲染列表时,将key={i}改为key={t.id},确保每个列表项的key唯一且稳定:

// 原代码
{enhancedListItems.map((t, i) => (
    <div key={i}>
        ...
    </div>
))}

// 修改后
{enhancedListItems.map((t) => (
    <div key={t.id}>
        ...
    </div>
))}

2. 状态存储数据而非React元素

修改App.tsx的状态和渲染逻辑,只存储列表项数据,渲染时动态生成ListItem组件:

import React, { useState } from 'react';
import ListItem from "./list_item";

const testData = [
    {
        id: 0,
        value: 1,
    }
]
type ListItemData = {
    id: number;
    defaultValue: number;
}

function App() {
    // 状态只存储数据
    const [listItems, setListItems] = useState<ListItemData[]>(testData);

    function onAddListItemButtonClick(): void {
        const newId = listItems.reduce((p: number, c) => Math.max(p, c.id), 0) + 1;
        const newItem = { 
            id: newId, 
            defaultValue: Math.floor(Math.random() * 10) 
        };
        setListItems([...listItems, newItem]);
    }

    function onRemoveButtonClick(id: number): void {
        setListItems(listItems.filter(t => t.id !== id));
    }

    return (
        <div>
            {listItems.map((item) => (
                <div key={item.id}>
                    <ListItem id={item.id} defaultValue={item.defaultValue.toString()} />
                    <button onClick={() => onRemoveButtonClick(item.id)}>-</button>
                </div>
            ))}
            <button onClick={onAddListItemButtonClick}>+</button>
        </div>
    );
}

export default App;

3. 简化ListItem组件(可选)

如果不需要ref,可以去掉forwardRef,简化组件:

import React, { useState } from "react";

export type ListItemProps = {
    id?: number;
    defaultValue?: string;
}

function ListItem(props: ListItemProps): React.ReactElement {
    const [value, setValue] = useState(props.defaultValue ?? "");

    function onInputChange(e: React.ChangeEvent<HTMLInputElement>): void {
        setValue(e.target.value);
    }

    return (
        <div>
            <span>{props.id}</span>
            <input type="text" value={value} onChange={onInputChange} />
        </div>
    );
}

export default ListItem;

简化版代码修复

针对你提供的简化代码,仅修改key即可解决问题:

import React, { useState } from 'react';

type EnhancedListItem = {
    id: number;
}

function App() {
    const [enhancedListItems, setEnhancedListItems] = useState<EnhancedListItem[]>([]);

    function onAddListItemButtonClick(): void {
        const newId = enhancedListItems.reduce((p: number, c) => Math.max(p, c.id), 0) + 1;
        setEnhancedListItems([...enhancedListItems, { id: newId }]);
    }

    function onRemoveButtonClick(id: number): void {
        setEnhancedListItems(enhancedListItems.filter(t => t.id !== id));
    }

    return (
        <div>
            {enhancedListItems.map((t) => (
                // 把key从i改成t.id
                <div key={t.id}>
                    <span>{t.id}</span>
                    <input type="text" />
                    <button onClick={() => onRemoveButtonClick(t.id)}>-</button>
                </div>
            ))}
            <button onClick={onAddListItemButtonClick}>+</button>
        </div>
    );
}

export default App;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 21:54:59