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

React循环渲染时useState状态不更新、表单值串扰问题

问题描述

在循环解析API返回数据的场景下,需要根据字段特性动态加载对应表单组件,但使用useState()做状态绑定后,TextField与Checkbox组件始终无法正确响应更新:操作任意一个表单项后,值会异常同步到其他同类组件上;切换加载不同条目的数据时,组件不会读取新条目对应的值,始终复用第一次加载的数据。

复现现象

首次加载数据为 { "ID": 1, "name": "x", "category": "x1", "default": true } 时,状态能正常捕获初始值;切换加载另一条数据 { "ID": 34, "name": "z", "category": "z34", "default": false } 时,组件不会加载新条目自身的取值,始终复用第一条数据的值,所有动态渲染的表单项都存在该异常。

问题复现代码如下:

import * as React from 'react';
import { Box, Checkbox, TextField } from '@mui/material';

export interface IFormsProps {
  columns: any;
  data: any;
  typeElement: any;
}

export default function FormEdit(props: any) {
  const { columns, data, typeElement } = props;

  const keys = Object.keys(data);
  let counter = 0;
  const box: any = [];

  if (keys.length > 0) {
    for (let pos = 0; pos < columns.length; pos++) {
      // 错误:在循环/条件判断内调用Hook,违反React Hook规则
      const [checked, setChecked] = React.useState(data[keys[pos]]);
      const [content, setData] = React.useState(data[keys[pos]]);

      const changeValue = (e: React.ChangeEvent<HTMLInputElement>) => {
        setData(e.target.value);
      };

      const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setChecked(!e.target.checked);
      };

      switch (typeElement[keys[counter++]].type) {
        case 'textBox':
          box.push(
            <TextField
              color="warning"
              InputLabelProps={{ shrink: true }}
              id="outlined"
              label={columns[pos].headerName}
              value={content}
              onChange={changeValue}
            />,
          );
          break;
        case 'checkBox':
          box.push(
            <div>
              <Checkbox
                color="success"
                checked={checked}
                onChange={handleChange}
                inputProps={{ 'aria-label': 'controlled' }}
              />
              active
            </div>,
          );
          break;
        case 'numberBox':
          box.push(
            <TextField
              color="warning"
              id="outlined-number"
              label={columns[pos].headerName}
              type="number"
              onChange={changeValue}
              value={content}
              InputLabelProps={{
                shrink: true,
              }}
            />,
          );
          break;
        case 'idBox':
          box.push(
            <TextField
              InputLabelProps={{ shrink: true }}
              id="outlined"
              disabled
              label={columns[pos].headerName}
              value={data[keys[pos]]} 
            />,
          );
          break;
        default:
          break;
      }
    }
  }

  return (
    <Box
      component="form"
      sx={{
        '& .MuiTextField-root': { m: 1, width: '29ch' },
      }}
      noValidate
      autoComplete="off"
    >
      {box}
    </Box>
  );
}
问题根因
  • 违反React核心Hook规则:禁止在循环、条件判断、嵌套函数中调用Hook。React依靠Hook的调用顺序匹配组件对应的状态存储,在循环里条件性调用useState会导致Hook调用顺序错乱,所有表单项的状态映射完全混乱,最终出现值互相覆盖、不更新的问题。
  • 零散拆分的独立state不适合动态表单场景,没有做外部props变化的同步逻辑:useState的初始值仅在组件第一次渲染时生效,后续传入新的data时,本地状态不会自动更新,自然无法加载新条目的值。
  • 原代码Checkbox更新逻辑写反:setChecked(!e.target.checked)会导致勾选状态和用户操作完全反向。
修复方案
  1. 把所有表单值统一存在顶层的单一state对象中,用字段名做key存储对应值,仅在组件顶层调用一次useState,完全符合Hook调用规则。
  2. 监听外部传入data的变化,当传入新条目数据时同步重置表单状态,保证切换数据时能正确加载新值。
  3. 抽离通用值更新逻辑,修改值时仅更新对应字段的状态,不会影响其他表单项。
  4. 给循环渲染的每个表单项加唯一key,帮助React正确识别组件更新,同时修复重复id、数字类型转换、手动计数索引错位等隐性问题。

修复后完整代码

import * as React from 'react';
import { Box, Checkbox, TextField } from '@mui/material';

export interface IFormsProps {
  columns: any;
  data: any;
  typeElement: any;
}

export default function FormEdit(props: IFormsProps) {
  const { columns, data, typeElement } = props;
  // 顶层统一存储所有表单值,仅调用一次useState
  const [formValues, setFormValues] = React.useState<Record<string, any>>({});

  // 同步外部传入的data到本地表单状态
  React.useEffect(() => {
    setFormValues({...data});
  }, [data]);

  // 通用字段更新方法
  const updateFieldValue = (fieldKey: string, value: any) => {
    setFormValues(prev => ({
      ...prev,
      [fieldKey]: value
    }));
  };

  // 统一生成表单项
  const formItems = React.useMemo(() => {
    if (!data || !columns?.length) return [];
    const keys = Object.keys(data);
    
    return columns.map((col: any, pos: number) => {
      const fieldKey = keys[pos];
      const fieldType = typeElement[fieldKey]?.type;
      const fieldValue = formValues[fieldKey];

      switch (fieldType) {
        case 'textBox':
          return (
            <TextField
              key={fieldKey}
              color="warning"
              InputLabelProps={{ shrink: true }}
              id={`outlined-${fieldKey}`}
              label={col.headerName}
              value={fieldValue ?? ''}
              onChange={(e) => updateFieldValue(fieldKey, e.target.value)}
            />
          );
        case 'checkBox':
          return (
            <div key={fieldKey}>
              <Checkbox
                color="success"
                checked={!!fieldValue}
                onChange={(e) => updateFieldValue(fieldKey, e.target.checked)}
                inputProps={{ 'aria-label': 'controlled' }}
              />
              active
            </div>
          );
        case 'numberBox':
          return (
            <TextField
              key={fieldKey}
              color="warning"
              id={`outlined-number-${fieldKey}`}
              label={col.headerName}
              type="number"
              value={fieldValue ?? ''}
              onChange={(e) => updateFieldValue(fieldKey, e.target.value === '' ? '' : Number(e.target.value))}
              InputLabelProps={{
                shrink: true,
              }}
            />
          );
        case 'idBox':
          return (
            <TextField
              key={fieldKey}
              InputLabelProps={{ shrink: true }}
              id={`outlined-id-${fieldKey}`}
              disabled
              label={col.headerName}
              value={data[fieldKey] ?? ''} 
            />
          );
        default:
          return null;
      }
    }).filter(Boolean);
  }, [columns, data, typeElement, formValues]);

  return (
    <Box
      component="form"
      sx={{
        '& .MuiTextField-root': { m: 1, width: '29ch' },
      }}
      noValidate
      autoComplete="off"
    >
      {formItems}
    </Box>
  );
}

内容的提问来源于stack exchange,提问作者Martín LC

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 09:24:34