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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 09:04:57