MUI DataGrid中TextField值丢失问题排查(React Hook Form受控场景)
问题排查:React Hook Form结合MUI DataGrid时TextField值丢失
- 场景:基于React Hook Form(搭配Controller、useFieldArray)实现的表单中,MUI DataGrid的
component列Select组件可正常更新嵌套数组字段值,但value列的TextField输入值后,离开编辑单元格时值会消失,且对应field.onChange事件未触发。
相关代码
import { FunctionComponent, useEffect, useState } from "react"; import { useForm, Controller, Handler, useFieldArray } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { DataGrid } from '@mui/x-data-grid/DataGrid'; import { GridColDef, GridRenderCellParams, GridRowId, GridRowParams, GridRowSelectionModel, GridCellEditStopParams, GridCellEditStopReasons, MuiEvent, GridCellParams, GridRenderEditCellParams, } from '@mui/x-data-grid/models'; import { CoPresentOutlined } from "@mui/icons-material"; interface TextFieldConfigurationProps { } const TextField: FunctionComponent<TextFieldConfigurationProps> = () => { const { handleSubmit, control, clearErrors, reset, setValue, getValues, watch, register, formState: { errors, isDirty: isFormDirty, touchedFields: isFormTouchedFields, dirtyFields } } = useForm<FileNamingConfigurationFormSchemaType>({ resolver: zodResolver(FileNamingConfigurationFormSchema), mode: 'onBlur', defaultValues: { name:'', nestedArray: [{ component: '', value: '' }] } }); const { fields, append, prepend, remove, swap, move, insert } = useFieldArray({ control, name: "nestedArray", }); const CountButton = (params: GridRenderCellParams<any, string>) => { console.log(params) console.log(control) return ( <FormControl sx={{ marginTop: 2, width: '100%' }} > <Controller control={control} name={`nestedArray.${Number(params.row.id)}.value`} render={({ field: { onChange, onBlur, value, ref } }) => ( < TextField id={`nestedArray.${Number(params.row.id)}.value`} value={value} inputRef={ref} onBlur={onBlur} onChange={onChange} value={value} onChange={e => { alert("enter") setValue(`nestedArray.${Number(params.row.id)}.value`, e.target.value) field.onChange(e.target.value); }} > </TextField > )} /> </FormControl > ); }; const columns: GridColDef[] = [ { field: 'component', headerName: 'file Component', width: 140, editable: true, renderCell: (params: GridRenderCellParams) => { return ( <FormControl sx={{ marginTop: 2, width: '100%' }}> <Controller control={control} name={`nestedArray.${Number(params.row.id)}.component`} render={({ field, }) => ( < TextField required {...field} id={`nestedArray.${Number(params.row.id)}.component`} select error={!!errors.nestedArray?.[Number(params.row.id)]?.component} onChange={e => { setValue(`nestedArray.${Number(params.row.id)}.fileField`, '') field.onChange(e.target.value); }} onBlur={field.onBlur} value={field.value} inputRef={field.ref} helperText={errors.nestedArray?.[Number(params.row.id)]?.component?.message} > <MenuItem disabled value=""> <em> Select One..</em> </MenuItem> </TextField> )} /> </FormControl> ) }, }, { field: 'value', headerName: 'Value/Starts With', width: 70, editable: true, renderCell: (params: GridRenderCellParams<any, string>) => { return <CountButton {...params} />; } } ] const submit: Handler<FileNamingConfigurationFormSchemaType> = async (data: FileNamingConfigurationFormSchemaType) => { try { console.log(data) } catch (error) { console.error('Validation failed:', error) } } const handleCancel = () => { if (isFormDirty) { } else { navigate("/") } } return <> <form noValidate autoComplete="off" onSubmit={handleSubmit(submit)} > <Box sx={{ padding: '3vh 2vh 4vh 2vh' }}> < FormControl sx={{ marginTop: 2, marginRight: 2.5, width: '23%' }}> <div style={labelStyle}> {`name`}</div> <Controller control={control} name="=name" render={({ field, }) => ( < TextField id="state" required {...field} select error={!!errors.name} onChange={e => { field.onChange(e.target.value); if (e.target.value.trim() !== '') { setValue('layout', '') setOnStateChange(e.target.value) } }} onBlur={field.onBlur} value={field.value} inputRef={field.ref} helperText={errors.name ? errors.name.message : "Select name"} > <MenuItem sx={{ height: 18, padding: '8px 12px 8px 12px' }} disabled value=""> <em> Select One..</em> </MenuItem> </TextField> )} /> </FormControl> <Stack sx={{ height: '75px', marginTop: '2vh' }} direction="row" justifyContent="center" alignItems="center" spacing={2} > {<Button sx={{ textTransform: 'none' }} variant="contained" onClick={() => append({ component: '', value: '' })}> Add</Button>} {<Button sx={{ textTransform: 'none' }} type="submit" variant="contained"> Save</Button>} {<CancelButton onClick={handleCancel}></CancelButton>} </Stack> <DataGrid rowHeight={80} autoHeight rows={fields.map((mapping, index) => ({ id: index, component: mapping.component, value: mapping.value }))} columns={columns} /> </Box> </form> </>; }; export default TextField;
问题根源
- 重复定义属性导致覆盖:
CountButton中的TextField同时写了onChange={onChange}和自定义onChange回调,后者直接覆盖前者,且自定义回调中引用了未在当前作用域定义的field变量,导致field.onChange执行报错,无法更新表单状态。 - 冗余的状态更新调用:自定义回调中同时使用
setValue和field.onChange,且setValue的调用逻辑存在冲突,React Hook Form的Controller已通过field.onChange处理状态更新,无需额外调用setValue。 - name属性笔误:name字段的Controller中
name="=name"多了一个等号,导致表单状态字段名错误,无法正确绑定值。 - rows映射逻辑隐患:DataGrid的row.id使用数组index,当数组项顺序变化时,会导致字段绑定错误。
修复方案
1. 修复CountButton组件
移除重复属性,正确使用Controller提供的field参数:
const CountButton = (params: GridRenderCellParams<any, string>) => { // 通过useFieldArray生成的唯一id找到对应索引,避免index变化导致的绑定错误 const fieldIndex = fields.findIndex(f => f.id === params.row.id); return ( <FormControl sx={{ marginTop: 2, width: '100%' }}> <Controller control={control} name={`nestedArray.${fieldIndex}.value`} render={({ field }) => ( // 直接展开field属性,简化代码并确保事件绑定正确 <TextField id={`nestedArray.${fieldIndex}.value`} {...field} /> )} /> </FormControl> ); };
2. 修正name字段的Controller
移除多余的等号,确保字段名正确:
<Controller control={control} name="name" render={({ field, }) => ( <TextField id="state" required {...field} select error={!!errors.name} onChange={(e) => { field.onChange(e.target.value); if (e.target.value.trim() !== '') { setValue('layout', ''); // 若setOnStateChange未定义需移除或补充定义 // setOnStateChange(e.target.value); } }} helperText={errors.name ? errors.name.message : "Select name"} > <MenuItem sx={{ height: 18, padding: '8px 12px 8px 12px' }} disabled value=""> <em> Select One..</em> </MenuItem> </TextField> )} />
3. 优化DataGrid的rows映射
使用useFieldArray生成的唯一id作为row.id,避免顺序变化导致的绑定错误:
rows={fields.map((mapping) => ({ id: mapping.id, component: mapping.component, value: mapping.value }))}
内容的提问来源于stack exchange,提问作者ashok
相关产品推荐
相关产品推荐

