如何在React Final Form字段数组中实现唯一Featured标识功能?
迁移React Final Form字段数组的Featured单选项逻辑实现方案
我们正在把个人资料编辑器表单从Redux Form迁移到React Final Form,表单里有个存储网站列表的字段数组,要求数组里只能有一个网站的featured属性为true。之前用Redux的action实现这个逻辑,现在要移除Redux保留功能。当前组件基于react-final-form-arrays和react-beautiful-dnd做了可拖拽的字段数组,希望Featured功能有可扩展性,我们列了几个潜在方案,求实现经验。
相关数据格式
websites: [ { url: "https://yikes.com", name: "Example 1", featured: true }, { url: "https://foo.bar", name: "Example 2", featured: false }, ... ]
原Redux实现代码
export const setItemAsFeatured = j => (dispatch, getState) => { // set all the other items as featured=false const items = selector(getState(), "items"); items.forEach((_, i) => { if (i !== j) { dispatch(change(FORM, `items[${i}].featured`, false)); } }); };
当前组件实现代码
const makeOnDragEndFunction = fields => result => { // dropped outside the list if (!result.destination) { return; } fields.move(result.source.index, result.destination.index); }; export function DraggableFieldArrayBase(props) { const { name, classes, featuredOption } = props; return ( <FieldArray name={name}> {({ fields }) => ( <DragDropContext onDragEnd={makeOnDragEndFunction(fields)}> <Droppable droppableId="droppable"> {(dProvided, snapshot) => ( <div ref={dProvided.innerRef} style={getListStyle(snapshot.isDraggingOver)} className="draggable__container" > {fields.map((field, index) => ( <Draggable className={`${classes}`} tabindex={0} key={field} draggableId={field} index={index}> {(provided, snapshot) => ( <div className="profile-edit__field-group draggable__field-group" ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)} > <DragIndicatorIcon className="draggable__indicator" /> <props.fieldComponent name={field} index={index} fields={fields} featuredOption={featuredOption} classes="draggable__component" /> <RemoveItemButton srText={`Remove item number ${index + 1} of ${fields.name.replace(".", ", ")}`} onClick={() => fields.remove(index)} /> {provided.placeholder} </div> )} </Draggable> ))} {dProvided.placeholder} </div> )} </Droppable> </DragDropContext> )} </FieldArray> ); }
潜在方案
- 为字段添加onChange函数,访问表单状态,将数组中其他项的featured设为false
- 字段级校验,若多个featured被勾选则提示错误
- 提交时执行上述校验
推荐实现方案
方案1:onChange联动修改(最优,贴合原交互体验)
直接在fieldComponent里给featured字段绑定onChange事件,通过React Final Form的form对象修改其他项的featured值,完全复刻原Redux逻辑,用户勾选一个featured时自动取消其他项,体验流畅。
具体实现:
// 在fieldComponent内部 import { useForm } from 'react-final-form'; const WebsiteField = ({ name, index, fields }) => { const { change } = useForm(); const handleFeaturedChange = (e) => { const isChecked = e.target.checked; // 先设置当前项的featured值 change(`${name}.featured`, isChecked); if (isChecked) { // 遍历所有项,取消其他项的featured fields.forEach((field, i) => { if (i !== index) { change(`${field}.featured`, false); } }); } }; return ( <div> {/* 渲染url、name等其他字段 */} <label> 设为Featured <Field name={`${name}.featured`} type="checkbox" onChange={handleFeaturedChange} /> </label> </div> ); };
方案2:字段级校验(作为兜底保障)
如果担心onChange联动出现异常(比如异步修改、拖拽后索引变化),可以添加字段级校验作为兜底,实时或提交前提示用户数据不符合要求。
在父表单中添加校验规则:
const validate = (values) => { const errors = {}; if (values.websites) { const featuredCount = values.websites.filter(item => item.featured).length; if (featuredCount > 1) { errors.websites = '只能有一个网站设置为Featured'; // 给每个选中的featured项添加单独错误提示 values.websites.forEach((_, index) => { if (!errors[`websites[${index}]`]) errors[`websites[${index}]`] = {}; if (_.featured) { errors[`websites[${index}]`].featured = '已存在其他Featured网站'; } }); } } return errors; }; // 在Form组件中使用 <Form onSubmit={handleSubmit} validate={validate} // 其他props > {/* 表单内容 */} </Form>
方案3:提交时校验(仅兜底,体验较差)
只在提交时校验的话,用户操作后不会立即得到反馈,直到点击提交才会报错,体验不如前两种,仅建议作为最后的兜底方案,不单独使用。
扩展性建议
- 把Featured单选项逻辑封装成自定义hook,方便其他字段数组复用:
export const useSingleFeaturedFieldArray = (fields, change) => { const handleFeaturedChange = (currentIndex, isChecked) => { if (isChecked) { fields.forEach((field, i) => { if (i !== currentIndex) { change(`${field}.featured`, false); } }); } }; return { handleFeaturedChange }; };
- 拖拽后索引变化不影响逻辑,因为
fields.map会实时更新索引,handleFeaturedChange使用的是当前遍历的index,拖拽后重新渲染时会自动修正。
内容的提问来源于stack exchange,提问作者Tay Solis
相关产品推荐
相关产品推荐

