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

useForm如何为动态MCQ输入框批量设置API返回的表单值

react-hook-form 多选题编辑回显实现方案

问题原因

当前实现存在三个核心错误:

  1. 字段注册和数据结构不匹配:接口返回的mcqAnswer是对象数组,却注册了mcqAnswer、selectCorrectAnswer这类单值字段,没有按数组下标绑定每个选项的独立字段
  2. 动态选项逻辑不合理:把JSX组件直接存在state里,接口返回数据时无法同步更新组件数量和内部状态
  3. 赋值逻辑错误:data.mcqAnswer是数组,直接访问data.mcqAnswer.answer拿到的是undefined,没有遍历数组给每个选项赋值

实现步骤

1. 调整state逻辑,不要存JSX

选项列表state只存唯一标识id,不要直接存组件,避免状态更新不同步:

// 仅存储选项id,所有选项的answer、checked状态统一交给react-hook-form管理
const [mcqAnswerIds, setMcqAnswerIds] = useState([]);

新增/删除选项的逻辑同步调整:

// 新增选项
const handleCreateMcqQuestion = () => {
  const newId = Date.now(); // 生成唯一id,可替换为你原来的mcqIds生成逻辑
  setMcqAnswerIds(prev => [...prev, newId]);
  // 给新选项初始化默认值
  const newIndex = mcqAnswerIds.length;
  setValue(`mcqAnswer.${newIndex}`, { answer: "", checked: false });
};

// 删除选项
const handleDeleteMcqQuestion = (targetId, targetIndex) => {
  setMcqAnswerIds(prev => prev.filter(id => id !== targetId));
  clearErrors(`mcqAnswer.${targetIndex}`);
  // 删除后重新整理表单数组字段,避免下标错位
  const currentValues = getValues("mcqAnswer").filter((_, idx) => idx !== targetIndex);
  setValue("mcqAnswer", currentValues);
};

2. 接口返回数据时同步初始化

在监听接口数据的useEffect里,先初始化选项id列表,再直接给整个mcqAnswer字段赋值,不需要逐个字段设置:

useEffect(() => {
  if (!data?.mcqAnswer?.length) return;
  // 提取接口返回的选项id,自动生成对应数量的选项组件
  const answerIds = data.mcqAnswer.map(item => item.id);
  setMcqAnswerIds(answerIds);
  // 直接赋值整个数组,react-hook-form会自动匹配对应下标的字段
  setValue("mcqAnswer", data.mcqAnswer, {
    shouldValidate: false,
    shouldDirty: false,
    shouldTouch: false
  });
}, [data]);

3. 调整组件渲染和字段绑定

遍历mcqAnswerIds渲染选项组件,给每个组件传入对应的id和下标,不要把JSX存在state里:

// 选项列表渲染位置
{mcqAnswerIds.map((answerId, index) => (
  <McqQuestion
    key={answerId}
    answerId={answerId}
    index={index}
    onDelete={handleDeleteMcqQuestion}
  />
))}
<button onClick={handleCreateMcqQuestion}>新增选项</button>

修改McqQuestion组件内部的字段绑定,用字段名.下标.属性的格式注册,和数据结构保持一致:

// McqQuestion组件接收index、answerId、onDelete作为props
const McqQuestion = ({ index, answerId, onDelete }) => {
  return (
    <>
      <FormControl id="type" style={{ wordBreak: "break-word" }}>
        <Editor
          {...register(`mcqAnswer.${index}.answer`, { required: true })}
          theme="snow"
          className="pt-0.5 border rounded-md border-gray-200 w-full"
          value={watch(`mcqAnswer.${index}.answer`)}
          placeholder={t("Enter your question")}
          onChange={(content) => setValue(`mcqAnswer.${index}.answer`, content)}
        />
      </FormControl>
      <FormControl id="type">
        <div className="flex">
          <div className="flex w-1/2 mr-5">
            <div className="block w-full">
              <div className="flex flex-col gap-4 mt-1 text-sm">
                <div className="flex">
                  <input
                    {...register(`mcqAnswer.${index}.checked`)}
                    data-id={answerId}
                    className="form-check-input checked:bg-blue-600 checked:border-blue-600 focus:outline-none"
                    type="checkbox"
                    onChange={e => {
                      setValue(`mcqAnswer.${index}.checked`, e.target.checked);
                      // 如果是单选题(仅一个正确答案),打开下面注释,自动取消其他选项的勾选
                      // if (e.target.checked) {
                      //   const allValues = getValues("mcqAnswer");
                      //   allValues.forEach((_, idx) => {
                      //     if (idx !== index) setValue(`mcqAnswer.${idx}.checked`, false);
                      //   })
                      // }
                      handleCheckboxChange(e.target.checked, answerId);
                    }}
                  />
                  {watch(`mcqAnswer.${index}.checked`) ? (
                    <label
                      className="flex items-center text-xs font-semibold text-black cursor-pointer">
                      {t("The correct answer")}
                    </label>
                  ) : null}
                </div>
                <div className="" onClick={() => onDelete(answerId, index)}>
                  <label id={`remove-${answerId}`}>
                    <Remove />
                  </label>
                  <label
                    htmlFor={`remove-${answerId}`}
                    className="text-[#757D8A] cursor-pointer">
                    {t("Remove")}
                  </label>
                </div>
              </div>
            </div>
          </div>
        </div>
      </FormControl>
    </>
  )
}

优化建议

  • 不要把JSX元素存在state中,state仅存渲染所需的原始数据,渲染时动态生成JSX,能避免绝大多数状态不同步问题
  • 表单相关的状态统一交给react-hook-form管理,不要额外在组件state中存一份answer、checked值,避免双份状态不一致
  • react-hook-form原生支持嵌套对象、数组结构的字段,只要register的路径和数据结构一致,setValue可以直接传入整个结构,不需要逐个字段赋值

内容的提问来源于stack exchange,提问作者Ahmed Radi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 10:57:14