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
相关产品推荐
相关产品推荐

