useFieldArray搭配Material UI Select组件失效问题排查
react-hook-form useFieldArray + Material UI Select 动态表单异常修复
问题现象
用 react-hook-form 的 useFieldArray 实现带 Material UI Select 组件的动态表单时,TextField 功能正常,但 Select 组件存在两个问题:
- 设置的
defaultValue在 Select 中无法显示,TextField 正常; - 提交表单时,无法获取 Select 的选中值和 TextField 的输入值。
Select 组件代码
import * as React from "react"; import { FormControl, FormHelperText, InputLabel, MenuItem, OutlinedInput, Select, SelectChangeEvent } from "@mui/material"; import { useState } from "react"; import { useFormContext, Controller } from "react-hook-form"; import Placeholder from "./Placeholder"; export interface IOptionTypes { id: string; label: string; value: string; } interface IFormElementTypes { name: string; label: string; required?: boolean; defaultValue: string; options: IOptionTypes[]; placeholder: string; } export default function MultiSelectField({ name, label, required, defaultValue, options, placeholder }: IFormElementTypes) { const { control, register, formState: { errors } } = useFormContext(); const [selectedVal, setSelectedVal] = useState<string[]>([]); const handleChange = (event: SelectChangeEvent<typeof selectedVal>) => { const { target: { value } } = event; setSelectedVal( typeof value === "string" ? value.split(",") : value ); }; const labelText = `${label}${required ? "*" : ""}`; const labelId = `multi-select-${name}`; return ( <Controller name={name} defaultValue={defaultValue} control={control} render={({ field }) => ( <FormControl fullWidth> <InputLabel shrink sx={{ backgroundColor: "white", padding: "0px 2px" }} error={!!errors[name]} id={labelId} > {labelText} </InputLabel> <Select {...field} label={labelText} labelId={labelId} value={selectedVal} displayEmpty multiple {...register(`${name}` as const)} variant="outlined" fullWidth error={!!errors[name]} input={ <OutlinedInput id={`multi-select-${label}`} label={`${label}${required ? "*" : ""}`} /> } renderValue={ selectedVal.length > 0 ? undefined : () => <Placeholder>{placeholder}</Placeholder> } onChange={handleChange} > {options.map((option) => ( <MenuItem key={option.id} value={option.value}> {option.label} </MenuItem> ))} </Select> <FormHelperText>{String(errors[name]?.message ?? "")}</FormHelperText> </FormControl> )} /> ); }
调用代码
import * as React from "react"; import { useForm, useFieldArray, FormProvider, SubmitHandler } from "react-hook-form"; import { Button, Grid, Stack, Box } from "@mui/material"; import MultiSelectField, { IOptionTypes } from "./MultiSelectField"; import TextFieldElement from "./TextFieldElement"; type FormValues = { items: { name: string; age: string }[]; }; const defaultValues = { items: [{ name: "john", age: "10" }] }; const names: IOptionTypes[] = [ { id: "1", label: "John", value: "john" }, { id: "2", label: "Smith", value: "smith" }, { id: "3", label: "Julia", value: "julia" }, { id: "4", label: "David", value: "david" } ]; export default function App() { const methods = useForm<FormValues>({ defaultValues, mode: "all" }); const { control, formState, setError } = methods; const { fields, append, remove } = useFieldArray({ name: "items", control }); const formSubmitHandler: SubmitHandler<FormValues> = async ( data: FormValues ) => { console.log("Form values ", data); }; return ( <Grid container> <Grid item xs={12}> <Stack direction="row" spacing={2} my={2}> <Button variant="outlined" color="primary" type="submit" disableElevation fullWidth onClick={() => { append({ name: "", age: "" }); }} > Add Field </Button> <Button variant="outlined" color="primary" type="submit" disableElevation fullWidth onClick={() => { remove(fields.length - 1); }} > Remove Field </Button> </Stack> </Grid> <FormProvider {...methods}> <form onSubmit={methods.handleSubmit(formSubmitHandler)}> <Grid item xs={12}> <Stack spacing={2}> {fields.map((field, index) => ( <Stack direction="row" spacing={2} key={field.id}> <MultiSelectField name={`items.${index}.name`} label="Choose Name" placeholder="Name" defaultValue="" options={names} /> <TextFieldElement name={`items.${index}.age`} label="Input Age" placeholder="Age" /> </Stack> ))} </Stack> </Grid> <Grid container my={2}> <Grid item> <Button variant="outlined" color="primary" type="submit" disableElevation fullWidth > Save Changes </Button> </Grid> </Grid> </form> </FormProvider> </Grid> ); }
问题原因
- 本地状态与表单状态脱节:自定义的
selectedVal本地状态没有和 react-hook-form 提供的字段状态同步,默认值无法从表单状态映射到组件,选中值也无法更新到表单状态。 - 重复注册字段:同时使用
Controller和register,导致表单状态管理冲突,Controller已负责字段注册和状态同步,无需重复调用register。 - 类型不匹配:Select 是多选模式,值类型应为数组,但传入的
defaultValue是字符串,初始值类型也不匹配。 - 上下文范围错误:
FormProvider没有包裹添加/删除按钮,且表单字段部分在上下文外,导致字段无法正确关联表单状态。
修复步骤
1. 移除本地状态,同步表单状态
删除 selectedVal 本地状态,改用 Controller 提供的 field.value,修改 onChange 逻辑更新表单状态:
export default function MultiSelectField({ name, label, required, defaultValue, options, placeholder }: IFormElementTypes) { const { control, formState: { errors } } = useFormContext(); const labelText = `${label}${required ? "*" : ""}`; const labelId = `multi-select-${name}`; return ( <Controller name={name} defaultValue={defaultValue} control={control} render={({ field }) => { const handleChange = (event: SelectChangeEvent<string[]>) => { const { value } = event.target; const newValue = typeof value === "string" ? value.split(",") : value; field.onChange(newValue); }; return ( <FormControl fullWidth> <InputLabel shrink sx={{ backgroundColor: "white", padding: "0px 2px" }} error={!!errors[name]} id={labelId} > {labelText} </InputLabel> <Select {...field} label={labelText} labelId={labelId} value={field.value} displayEmpty multiple variant="outlined" fullWidth error={!!errors[name]} input={ <OutlinedInput id={`multi-select-${label}`} label={labelText} /> } renderValue={ field.value.length > 0 ? undefined : () => <Placeholder>{placeholder}</Placeholder> } onChange={handleChange} > {options.map((option) => ( <MenuItem key={option.id} value={option.value}> {option.label} </MenuItem> ))} </Select> <FormHelperText>{String(errors[name]?.message ?? "")}</FormHelperText> </FormControl> ); }} /> ); }
2. 修正类型定义与默认值
更新 IFormElementTypes 接口的 defaultValue 类型为数组,同时调整初始值和调用时的参数:
// 修正接口 interface IFormElementTypes { name: string; label: string; required?: boolean; defaultValue: string[]; options: IOptionTypes[]; placeholder: string; } // 修正表单初始值 const defaultValues = { items: [{ name: ["john"], age: "10" }] }; // 调用时传递正确的默认值 <MultiSelectField name={`items.${index}.name`} label="Choose Name" placeholder="Name" defaultValue={field.name || []} options={names} />
3. 调整 FormProvider 范围
将 FormProvider 包裹整个表单相关内容,包括添加/删除按钮:
export default function App() { const methods = useForm<FormValues>({ defaultValues, mode: "all" }); const { control, formState, setError } = methods; const { fields, append, remove } = useFieldArray({ name: "items", control }); const formSubmitHandler: SubmitHandler<FormValues> = async ( data: FormValues ) => { console.log("Form values ", data); }; return ( <FormProvider {...methods}> <Grid container> <Grid item xs={12}> <Stack direction="row" spacing={2} my={2}> <Button variant="outlined" color="primary" disableElevation fullWidth onClick={() => { append({ name: [], age: "" }); }} > Add Field </Button> <Button variant="outlined" color="primary" disableElevation fullWidth onClick={() => { remove(fields.length - 1); }} > Remove Field </Button> </Stack> </Grid> <form onSubmit={methods.handleSubmit(formSubmitHandler)}> <Grid item xs={12}> <Stack spacing={2}> {fields.map((field, index) => ( <Stack direction="row" spacing={2} key={field.id}> <MultiSelectField name={`items.${index}.name`} label="Choose Name" placeholder="Name" defaultValue={field.name || []} options={names} /> <TextFieldElement name={`items.${index}.age`} label="Input Age" placeholder="Age" /> </Stack> ))} </Stack> </Grid> <Grid container my={2}> <Grid item> <Button variant="outlined" color="primary" type="submit" disableElevation fullWidth > Save Changes </Button> </Grid> </Grid> </form> </Grid> </FormProvider> ); }
效果验证
修复后:
- Select 组件会正确显示默认选中值;
- 提交表单时,能正常获取 Select 的选中数组和 TextField 的输入值;
- 动态添加/删除字段功能正常。
内容的提问来源于stack exchange,提问作者iphonic
相关产品推荐
相关产品推荐

