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

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;

问题根源

  1. 重复定义属性导致覆盖:CountButton中的TextField同时写了onChange={onChange}和自定义onChange回调,后者直接覆盖前者,且自定义回调中引用了未在当前作用域定义的field变量,导致field.onChange执行报错,无法更新表单状态。
  2. 冗余的状态更新调用:自定义回调中同时使用setValue和field.onChange,且setValue的调用逻辑存在冲突,React Hook Form的Controller已通过field.onChange处理状态更新,无需额外调用setValue。
  3. name属性笔误:name字段的Controller中name="=name"多了一个等号,导致表单状态字段名错误,无法正确绑定值。
  4. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 11:16:09