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

React Hook Form:如何在useFieldArray列表间移动项?

在React Hook Form的不同列表间移动子项的解决思路

我正在使用React Hook Form构建一款基础页面构建器应用,目前使用useFieldArray钩子创建包含子项的列表,但尚未找到在不同列表间移动子项的方法。我了解可以使用move()函数在同一列表内重新排序子项,但由于每个列表都有独立的嵌套useFieldArray,无法将子项从一个列表组件迁移至另一个列表组件。以下是我当前的简化代码结构:

export const App = () => {
    const methods = useForm({
        defaultValues: {
            lists: [
                {
                    list_id: 1,
                    items: [
                        {
                            item_id: 1,
                            name: 'Apple'   
                        },
                        {
                            item_id: 2,
                            name: 'Orange'    
                        }
                    ]
                },
                {
                    list_id: 2,
                    items: [
                        {
                            item_id: 3,
                            name: 'Banana'   
                        },
                        {
                            item_id: 4,
                            name: 'Lemon'   
                        }
                    ]
                }             
            ]    
        }
    });
    return (
        <FormProvider {...methods}>
            <Page/>
        </FormProvider>
    )
}
export const Page = () => {
    const { control } = useFormContext();
    const { fields } = useFieldArray({
        control,
        name: 'lists'
    })

    return (
        <ul>
            {fields?.map((field, index) => (
                <List listIdx={index} />
            ))}
        </ul>
    )
}
export const List = ({ listIdx }) => {
    const { control, watch } = useFormContext();
    const { fields, move } = useFieldArray({
        control,
        name: `lists[${listIdx}].items`
    })

    const handleMove = (prevIdx, nextIdx) => {
        // 仅支持列表内移动,无法跨列表
        move(prevIdx, nextIdx);
    }

    return (
        <li>
            <p>ID: {watch(`lists[${listIdx}].list_id`)}</p>
            <ul>
                {fields?.map((field, index) => (
                    <Item listIdx={listIdx} itemIdx={index} handleMove={handleMove}/>
                ))}
            </ul>
        </li>
    )
}
export const Item = ({ listIdx, itemIdx, handleMove }) => {
    const { register, watch } = useFormContext();

    return (
        <li>
            <p>ID: {watch(`lists[${listIdx}].items[${itemIdx}].item_id`)}</p>
            <label>
                Name:
                <input {...register(`lists[${listIdx}].items[${itemIdx}].name`)} />
            </label>
            <button onClick={() => handleMove(itemIdx, itemIdx - 1)}>Up</button>
            <button onClick={() => handleMove(itemIdx, itemIdx + 1)}>Down</button>
        </li>
    )
}

解决思路

核心逻辑是在父组件统一管理跨列表的数据操作,因为子组件的useFieldArray仅能操作当前列表的子项,而父组件可以获取整个表单的数据源,通过getValues和setValue实现跨列表的子项迁移。

1. 改造Page组件,添加跨列表移动方法

将跨列表移动的逻辑放在Page组件中,这里可以获取到完整的表单数据,以及修改数据的方法:

export const Page = () => {
    const { control, getValues, setValue } = useFormContext();
    const { fields: listsFields } = useFieldArray({
        control,
        name: 'lists'
    });

    // 跨列表移动子项:源列表索引、源子项索引、目标列表索引
    const moveItemBetweenLists = (sourceListIdx, sourceItemIdx, targetListIdx) => {
        // 获取当前表单的lists数据并浅拷贝,避免直接修改原始状态
        const currentLists = [...getValues('lists')];
        // 从源列表取出子项
        const [movedItem] = currentLists[sourceListIdx].items.splice(sourceItemIdx, 1);
        // 将子项插入目标列表
        currentLists[targetListIdx].items.push(movedItem);
        // 更新表单数据
        setValue('lists', currentLists);
    };

    return (
        <ul>
            {listsFields?.map((field, index) => (
                <List 
                    key={field.id} 
                    listIdx={index} 
                    moveItemBetweenLists={moveItemBetweenLists} 
                />
            ))}
        </ul>
    );
};

2. 更新List组件,传递跨列表方法

让List组件接收跨列表移动的方法,并传递给子组件Item:

export const List = ({ listIdx, moveItemBetweenLists }) => {
    const { control, watch } = useFormContext();
    const { fields: itemsFields, move: moveWithinList } = useFieldArray({
        control,
        name: `lists[${listIdx}].items`
    });

    const handleMoveWithinList = (prevIdx, nextIdx) => {
        moveWithinList(prevIdx, nextIdx);
    };

    // 获取所有列表数据,用于Item组件选择目标列表
    const allLists = watch('lists');

    return (
        <li>
            <p>ID: {watch(`lists[${listIdx}].list_id`)}</p>
            <ul>
                {itemsFields?.map((field, itemIdx) => (
                    <Item
                        key={field.id}
                        listIdx={listIdx}
                        itemIdx={itemIdx}
                        handleMove={handleMoveWithinList}
                        moveItemBetweenLists={moveItemBetweenLists}
                        allLists={allLists}
                    />
                ))}
            </ul>
        </li>
    );
};

3. 改造Item组件,添加跨列表移动交互

给Item组件添加选择目标列表的下拉框和移动按钮:

export const Item = ({ listIdx, itemIdx, handleMove, moveItemBetweenLists, allLists }) => {
    const { register, watch } = useFormContext();
    const currentItemsLength = watch(`lists[${listIdx}].items`).length;

    return (
        <li>
            <p>ID: {watch(`lists[${listIdx}].items[${itemIdx}].item_id`)}</p>
            <label>
                Name:
                <input {...register(`lists[${listIdx}].items[${itemIdx}].name`)} />
            </label>
            {/* 列表内上下移动按钮,添加禁用逻辑 */}
            <button onClick={() => handleMove(itemIdx, itemIdx - 1)} disabled={itemIdx === 0}>Up</button>
            <button onClick={() => handleMove(itemIdx, itemIdx + 1)} disabled={itemIdx === currentItemsLength - 1}>Down</button>
            
            {/* 跨列表移动控件,只有存在多个列表时显示 */}
            {allLists?.length > 1 && (
                <>
                    <select id={`target-list-${listIdx}-${itemIdx}`}>
                        {allLists.map((list, idx) => 
                            idx !== listIdx && (
                                <option key={list.list_id} value={idx}>
                                    List {list.list_id}
                                </option>
                            )
                        )}
                    </select>
                    <button onClick={() => {
                        const targetIdx = parseInt(document.getElementById(`target-list-${listIdx}-${itemIdx}`).value);
                        moveItemBetweenLists(listIdx, itemIdx, targetIdx);
                    }}>Move to List</button>
                </>
            )}
        </li>
    );
};

关键注意点

  • 渲染列表和子项时必须加上key={field.id},这是React Hook Form的useFieldArray自动生成的唯一标识,避免重渲染导致的状态异常。
  • 使用getValues获取数据时要做浅拷贝,避免直接修改原始状态引发的表单同步问题。
  • 可以根据需求替换跨列表移动的交互方式(比如拖拽),核心逻辑都是取出子项+插入目标列表。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 18:20:20