react-hook-form如何为原生HTML多选select设置回显值
问题解决方案
该需求完全可以基于原生HTML元素实现,不需要引入任何第三方UI组件库,你遇到的问题来自3个易忽略的配置错误,按以下步骤修改即可:
核心问题原因
- 你在循环渲染所有表格行时,给每一行的输入框、多选框都绑定了相同的
name/sub_types注册名,react-hook-form同名字段会被最后渲染的实例覆盖,你调用setValue时无法命中当前要编辑行的控件 - 原生多选
<select>的option值默认是字符串类型,你传入的待选中id是数字类型,类型不匹配会导致严格相等判断失败,无法触发选中效果 - 注册
sub_types字段时没有设置正确的数组类型默认值,且setValue时缺少必要的触发配置,给option手动加selected属性也会被react-hook-form的内部受控状态覆盖,不会生效
具体修改步骤
- 新增状态存储当前正在编辑的工单类型ID,仅当前编辑行渲染可交互表单控件,其余行渲染普通文本,避免同名字段重复注册
- 注册
sub_types字段时设置默认值为空数组,如需保留数字类型的id,可通过valueAs相关配置做类型转换,或者赋值时统一把id转为字符串 - 赋值时直接传入所有需要选中的id组成的数组(注意和option的value类型保持一致),不要循环单次调用
setValue - 移除option上手动添加的
selected属性,选中状态完全由react-hook-form的表单状态控制
修改后的核心代码示例
import React, { useState, useEffect } from 'react'; import { useForm } from "react-hook-form"; import { ErrorMessage } from '@hookform/error-message'; import { RootStateOrAny, useSelector, useDispatch } from 'react-redux'; import { updateTicketTypeAction } from '../../actions/ticketActions'; interface ttFormInputs { name: string; // 明确子类型id的类型,如果你用字符串id就改成string[] sub_types: number[]; // 新增字段存当前编辑的类型id,提交时用 edit_id: number | null; } export default function Ticket() { // 新增状态控制当前展开编辑的行 const [editingId, setEditingId] = useState<number | null>(null) const { register, resetField, reset, formState: { errors }, handleSubmit, setValue } = useForm<ttFormInputs>({ // 给所有字段设置正确的默认值,这步非常重要 defaultValues: { name: '', sub_types: [], edit_id: null } }); const types = useSelector((state: RootStateOrAny) => state.root.ticket.types); const onToggleEdit = ((type: any) => { // 标记当前编辑的行id setEditingId(type.id) setValue('edit_id', type.id) setValue('name', type.name) // 过滤出connected=true的子项id,组成数组,注意如果option value是字符串就转成String(value.id) const selectedSubTypes = type.sub_types .filter((sub: any) => sub.connected) .map((sub: any) => sub.id) // 数字id直接传,字符串id就加.toString() // 赋值时加上配置确保视图更新 setValue('sub_types', selectedSubTypes, { shouldDirty: true, shouldTouch: true }) }) const dispatch = useDispatch() const onUpdateTicketType = handleSubmit(async (data: ttFormInputs) => { dispatch(updateTicketTypeAction(data)) // 提交后重置所有字段,退出编辑状态 reset() setEditingId(null) }) const typeList = types.map((type: any) => <tr key={ type.id } onClick={() => onToggleEdit(type)}> <td> { editingId === type.id ? ( <> <input {...register("name", { required: "名称为必填项"})} /> <ErrorMessage errors={errors} name="name" /> </> ) : type.name } </td> <td> { editingId === type.id ? ( // 注册多选select,不需要额外加配置,只要默认值是数组即可 <select {...register("sub_types")} multiple style={{height: '120px'}}> { type.sub_types.map((value: any) => ( // 这里value的类型要和你setValue传的数组元素类型完全一致,数字就用数字,字符串就用字符串 <option key={value.id} value={value.id}> {value.name} </option> )) } </select> ) : ( // 非编辑状态直接渲染已选中的子类型文本 type.sub_types.filter((sub:any)=>sub.connected).map((sub:any)=>sub.name).join('、') ) } </td> <td> { editingId === type.id && <input type="submit" /> } </td> <td> <button onClick={(e) => { e.stopPropagation() onDeleteTicketType(e, type.id) }}>删除</button> </td> </tr> ); return ( <div> <h4 className="pageTitle">工单配置</h4> <form onSubmit={onUpdateTicketType}> <table> <thead> <tr> <th>工单类型</th> <th>子类型</th> <th>操作</th> <th></th> </tr> </thead> <tbody> { types && typeList } </tbody> </table> </form> </div> ); }
额外注意点
- 原生多选select默认高度很小,只能看到一个选项,记得给它加个固定高度方便用户多选
- 测试多选时按住Ctrl/Command键点击option即可选中多个值
- 如果需要id保留数字类型但原生select传值总是字符串,可以在register的时候加解析配置:
这个配置会自动把提交的字符串数组转成数字数组,和你传入的数字类型id匹配。<select {...register("sub_types", { setValueAs: (v) => Array.isArray(v) ? v.map(Number) : [] })} multiple>
内容的提问来源于stack exchange,提问作者Dallas Caley
相关产品推荐
相关产品推荐

