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

react-hook-form自定义组件数组值处理:复选框选中状态丢失

问题

我使用react-hook-form的Controller组件,将字段与ref传入自定义组件以展示复选框数组,选中项对应为对象数组元素。组件渲染正常,但勾选复选框后,表单刷新会导致选中状态丢失。以下是我的自定义组件及表单代码,请问问题出在哪里?

自定义组件代码

type FormCheckBoxesParts = {
  options: Quality[]; onChange: (...event: any[]) => void; value: Quality[] | undefined; name: string; ref: Ref<Array<Quality>>;
}
const handleTraitCheck = (value: Spec[], checkedName: Spec) => {
  const existingValue = value.filter(qi => qi.name === checkedName.name);
  if (existingValue && existingValue.length === 1) {
    // then we are unchecking, so remove element from value array
    value.splice(value.indexOf(existingValue[0]),1)
  } else {
    // then we are checking, so add element from value array
    value.push(checkedName);
  }  
};


export const SpecArrayCheckboxes: FunctionComponent<FormCheckBoxesParts> = forwardRef(({ onChange, value, name, options }, ref) => {

  return (
    <ThemeProvider theme={strikerTheme}>
      <h5>{name}</h5>
      {
        options.map((option: Quality) => {
          return (
            <Tooltip title={option.description}>
              <FormControlLabel
                ref={ref}
                key={option.name}
                label={option.name}
                control={
                  <Checkbox
                    checked={value && value.some((fo: Quality) => fo.name === option.name)}
                    onChange={() => value && onChange(handleTraitCheck(value, option))}
                  />
                }
              />

            </Tooltip>
          );
        })
      }
    </ThemeProvider>
  )
});

表单代码

export const DescriptionForm = ({
  onSubmit,
  data
}: {
  data: Partial<Char>;
  onSubmit: SubmitHandler<Partial<Char>>;
}) => {

  const {
    specState
  } = useAppSelector(state => state);
  
  const { register, handleSubmit, control } = useForm({
    defaultValues: {
      ...data,
    }
  });

  const alignments = ['Lawful Good', 'Neutral Good', 'Chaotic Good', 'Lawful Neutral', 'Neutral', 'Chaotic Neutral', 'Lawful Evil', 'Neutral Evil', 'Chaotic Evil']
  const traits: Quality[] = getTraits(specState.specKeys);
  const ideals: Quality[] = getIdeals(specState.specKeys);

  return (

    <ThemeProvider theme={strikerTheme}>
      <form className='form' onSubmit={handleSubmit(onSubmit)}>
        <h4>Description</h4>
        <div className='form-row'>
          <Controller name="name" control={control} render={({ field }) => <TextField sx={{ width: '40ch' }} {...field} label="character name" />} />
        </div>
        <div className='form-row'>
          <Controller name="experience" control={control} render={({ field }) => <TextField {...field} label="experience" />} />
          <Controller name="levels" control={control} render={({ field }) => <TextField {...field} label="level" />} />
        </div>
        <div className='form-row'>
          <Controller name="age" control={control} render={({ field }) => <TextField {...field} label="age" />} />
          <Controller name="gender" control={control} render={({ field }) => <TextField {...field} label="gender" />} />
          <Controller name="height" control={control} render={({ field }) => <TextField {...field} label="height" />} />
          <Controller name="weight" control={control} render={({ field }) => 
            <TextField {...field} label="weight" InputProps={{
              endAdornment: <InputAdornment position="end">kg</InputAdornment>,
            }}
            />} 
          />
        </div>
        <div className='form-row'>
        <Controller
        name="alignment"
        control={control}
        render={({ field }) => <TextField {...field} label="alignment" helperText="Please select your alignment" select>
          {alignments.map((option) => (
            <MenuItem key={option} value={option}>
              {option}
            </MenuItem>
          ))}
        </TextField>}
        />
        <div className='form-row'>
          
          <Controller name="traits" control={control} render={({ field }) => <SpecArrayCheckboxes
            {...field}
            options={traits}/>
          } />
        </div>

        <div className='form-row'>
           
          <Controller name="ideals" control={control} render={({ field }) => <SpecArrayCheckboxes
            {...field}
            options={ideals}/>
           } />
        </div>

        </div>
        <button
          type='submit'
          className='btn btn-block btn-danger'
        >
          Submit
        </button>

      </form>
    </ThemeProvider>

  )
};
问题原因与解决方案

核心问题

  1. 可变操作导致状态无法追踪:handleTraitCheck里用splice和push直接修改了react-hook-form管理的value数组,这是可变操作。react-hook-form依赖不可变更新感知状态变化,因此不会触发表单状态同步,刷新后选中状态丢失。
  2. onChange未传递有效新值:handleTraitCheck没有返回新数组,onChange实际接收到的是undefined,根本没把更新后的状态同步给表单。
  3. ref使用错误:给每个FormControlLabel绑定同一个ref,不仅无效还会导致ref被覆盖;且react-hook-form的Controller已通过field.ref处理表单引用,无需手动传递给单个复选框。
  4. 类型不一致:handleTraitCheck参数用了Spec类型,但组件实际使用Quality类型,需要统一。

修正后的自定义组件代码

import { forwardRef, Ref, FunctionComponent } from 'react';
import { ThemeProvider } from '@mui/material/styles';
import Tooltip from '@mui/material/Tooltip';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';

// 统一类型,根据实际业务调整属性
type Quality = {
  name: string;
  description: string;
  // 其他属性
};

type FormCheckBoxesParts = {
  options: Quality[];
  onChange: (newValue: Quality[]) => void;
  value: Quality[] | undefined;
  name: string;
};

// 不可变更新:返回新数组,不修改原数组
const handleTraitCheck = (currentValue: Quality[], option: Quality): Quality[] => {
  const isChecked = currentValue.some(item => item.name === option.name);
  if (isChecked) {
    return currentValue.filter(item => item.name !== option.name);
  } else {
    return [...currentValue, option];
  }
};

export const SpecArrayCheckboxes = forwardRef<unknown, FormCheckBoxesParts>(({ onChange, value, name, options }, ref) => {
  // 处理value为undefined的情况,默认空数组
  const currentValue = value || [];

  return (
    <ThemeProvider theme={strikerTheme}>
      <h5>{name}</h5>
      {options.map((option) => (
        <Tooltip key={option.name} title={option.description}>
          <FormControlLabel
            label={option.name}
            control={
              <Checkbox
                checked={currentValue.some(item => item.name === option.name)}
                onChange={() => onChange(handleTraitCheck(currentValue, option))}
              />
            }
          />
        </Tooltip>
      ))}
    </ThemeProvider>
  );
});

SpecArrayCheckboxes.displayName = 'SpecArrayCheckboxes';

关键说明

  • 不可变更新:每次状态变化返回新数组,让react-hook-form能正确追踪状态,确保选中状态同步。
  • 正确传递新值:onChange直接接收handleTraitCheck返回的新数组,同步更新表单状态。
  • 移除无效ref:删除单个复选框上的ref绑定,依赖Controller自动处理表单引用。
  • 类型统一:修正Spec与Quality的类型不一致问题,避免类型错误。

内容的提问来源于stack exchange,提问作者Michael Coxon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 01:04:53