useForm如何为动态MCQ输入框批量设置API返回的表单值
react-hook-form 多选题编辑回显实现方案
问题原因
当前实现存在三个核心错误:
- 字段注册和数据结构不匹配:接口返回的
mcqAnswer是对象数组,却注册了mcqAnswer、selectCorrectAnswer这类单值字段,没有按数组下标绑定每个选项的独立字段 - 动态选项逻辑不合理:把JSX组件直接存在state里,接口返回数据时无法同步更新组件数量和内部状态
- 赋值逻辑错误:
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
相关产品推荐
相关产品推荐

