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

React组件中递归渲染嵌套对象字段的实现疑问

递归渲染React嵌套对象字段解决方案

我在开发React组件时遇到一个问题:现有包含嵌套对象的customerDraft对象,希望渲染该对象的所有字段(包括customerDraft.metadata内部的字段),但当前仅能渲染主对象的字段,metadata字段仅作为整体显示。需要通过递归方式实现嵌套字段的渲染,同时支持字段更新。


核心思路

抽离一个递归渲染组件专门处理对象字段的遍历与渲染:

  • 当字段值是对象(非null)时,递归调用自身展开嵌套字段
  • 当字段值是基础类型时,渲染输入框
  • 实现支持嵌套路径的状态更新逻辑,确保修改嵌套字段时能正确更新customerDraft

具体实现步骤

1. 定义递归渲染组件

在CustomerDetailEditModal组件内部新增ObjectFieldRenderer组件,负责单个对象的渲染逻辑:

const ObjectFieldRenderer = ({ obj, parentPath = '', onFieldChange, classes, restrictedData }) => {
  return Object.entries(obj).map(([key, value]) => {
    // 过滤限制字段:跳过含id或在restrictedData中的字段
    if (restrictedData.includes(key) || key.toLowerCase().includes('id')) {
      return null;
    }

    // 构建完整字段路径,比如metadata.birthDate
    const fullPath = parentPath ? `${parentPath}.${key}` : key;

    // 递归渲染嵌套对象
    if (typeof value === 'object' && value !== null) {
      return (
        <div key={fullPath} style={{ marginLeft: '16px', marginTop: '8px' }}>
          <h4 style={{ fontSize: '14px', fontWeight: 600, marginBottom: '8px' }}>{key}</h4>
          <ObjectFieldRenderer
            obj={value}
            parentPath={fullPath}
            onFieldChange={onFieldChange}
            classes={classes}
            restrictedData={restrictedData}
          />
        </div>
      );
    }

    // 渲染基础类型字段的输入框
    return (
      <CStandardInput
        key={fullPath}
        allowClear
        defaultValue={value}
        disableUnderline
        formclasses={{ root: classes.root }}
        htmlFor={`standard-customer-edit-${fullPath}`}
        id={`standard-customer-edit-${fullPath}`}
        label={fullPath}
        onChange={(event) => onFieldChange(fullPath, event.target.value)}
        size="large"
      />
    );
  });
};

2. 实现嵌套字段的更新逻辑

替换原有的顶层字段更新逻辑,新增支持嵌套路径的状态更新函数:

const handleFieldChange = (fieldPath, value) => {
  setDraftCustomer(prev => {
    // 递归更新嵌套对象的工具函数
    const updateNested = (obj, path, val) => {
      const [key, ...rest] = path.split('.');
      if (rest.length === 0) {
        return { ...obj, [key]: val };
      }
      return {
        ...obj,
        [key]: updateNested(obj[key] || {}, rest, val)
      };
    };
    return updateNested(prev, fieldPath, value);
  });

  // 保留原逻辑:fullName字段同时更新customerProfileDraft
  if (fieldPath === 'fullName') {
    setDraftProfile(prev => ({ ...prev, fullName: value }));
  }
};

3. 替换原有遍历逻辑

把原来Object.entries(customerDraft).map(...)的代码,替换为调用递归组件:

<ObjectFieldRenderer
  obj={customerDraft}
  onFieldChange={handleFieldChange}
  classes={classes}
  restrictedData={restrictedData}
/>

修改后的完整组件代码

const CustomerDetailEditModal = (props) => {
  const {
    open,
    setOpen,
    customerDraft,
    customerProfileDraft,
    setDraftCustomer,
    setDraftProfile,
    onUpdate
  } = props;

  const classes = useStyles();
  const dispatch = useDispatch();

  const [isPasswordHidden, setIsPasswordHidden] = useState(true);

  const projectId = useSelector(({ project }) => project.currentProject._id);
  const generatedPassword = useSelector(({ customer }) => customer.password);

  const isCurrentProjectCapstone = projectId === '4387564328756435';

  const onModalCancel = () => {
    setOpen(false);
    if (isCurrentProjectCapstone) {
      dispatch(removeItemFromCustomerDraftAction('password'));
    }
  };

  const generatePassword = () => {
    dispatch(getGeneratedPassword());
  };

  useEffect(() => {
    if (!generatedPassword) return;
    setDraftCustomer({
      ...customerDraft,
      password: generatedPassword
    });

    // eslint-disable-next-line
  }, [generatedPassword]);

  // 处理嵌套字段更新的函数
  const handleFieldChange = (fieldPath, value) => {
    setDraftCustomer(prev => {
      const updateNested = (obj, path, val) => {
        const [key, ...rest] = path.split('.');
        if (rest.length === 0) {
          return { ...obj, [key]: val };
        }
        return {
          ...obj,
          [key]: updateNested(obj[key] || {}, rest, val)
        };
      };
      return updateNested(prev, fieldPath, value);
    });

    if (fieldPath === 'fullName') {
      setDraftProfile(prev => ({ ...prev, fullName: value }));
    }
  };

  // 递归渲染对象字段的组件
  const ObjectFieldRenderer = ({ obj, parentPath = '', onFieldChange, classes, restrictedData }) => {
    return Object.entries(obj).map(([key, value]) => {
      if (restrictedData.includes(key) || key.toLowerCase().includes('id')) {
        return null;
      }

      const fullPath = parentPath ? `${parentPath}.${key}` : key;

      if (typeof value === 'object' && value !== null) {
        return (
          <div key={fullPath} style={{ marginLeft: '16px', marginTop: '8px' }}>
            <h4 style={{ fontSize: '14px', fontWeight: 600, marginBottom: '8px' }}>{key}</h4>
            <ObjectFieldRenderer
              obj={value}
              parentPath={fullPath}
              onFieldChange={onFieldChange}
              classes={classes}
              restrictedData={restrictedData}
            />
          </div>
        );
      }

      return (
        <CStandardInput
          key={fullPath}
          allowClear
          defaultValue={value}
          disableUnderline
          formclasses={{ root: classes.root }}
          htmlFor={`standard-customer-edit-${fullPath}`}
          id={`standard-customer-edit-${fullPath}`}
          label={fullPath}
          onChange={(event) => onFieldChange(fullPath, event.target.value)}
          size="large"
        />
      );
    });
  };

  return (
    <div>
      <Modal
        bodyStyle={{
          fontSize: '12px',
          height: 500,
          margin: '0 auto',
          overflowY: 'auto' // 新增滚动,避免嵌套字段过多溢出
        }}
        centered
        footer={
          <div
            style={{
              display: 'flex',
              justifyContent: 'flex-end'
            }}>
            <CButton
              htmlType="submit"
              onClick={(e) => {
                setOpen(false);
                e.preventDefault();
              }}
              size="large"
              type="secondary">
              Cancel
            </CButton>
            <CButton
              htmlType="submit"
              onClick={onUpdate}
              size="large"
              type="primary"
            >
              Save
            </CButton>
          </div>
        }
        onCancel={onModalCancel}
        title={
          <span
            style={{
              fontSize: '24px',
              fontWeight: 700,
              lineHeight: '24px'
            }}>
            Edit User
          </span>
        }
        visible={open}
        width={customerProfileDraft ? 770 : 385}>
        <form className={classes.form} id="customer-edit-form">
          <div className={classes.wrapperDiv}>
            {/* 调用递归渲染组件 */}
            <ObjectFieldRenderer
              obj={customerDraft}
              onFieldChange={handleFieldChange}
              classes={classes}
              restrictedData={restrictedData}
            />
            {isCurrentProjectCapstone && (
              <div className={classes.passwordWrapper}>
                <CStandardInput
                  adornment={
                    <>
                      <button
                        className={classes.buttonSvg}
                        onClick={() => {
                          navigator.clipboard.writeText(customerDraft.password || '');
                        }}
                        style={{
                          marginRight: '5px'
                        }}
                        type="button">
                        <img alt="copy password" src={copyIcon} />
                      </button>
                      <button
                        className={classes.buttonSvg}
                        onClick={() => setIsPasswordHidden(!isPasswordHidden)}
                        type="button">
                        <img
                          alt="toggle password visibility"
                          src={isPasswordHidden ? crossedEyeIcon : eyeIcon}
                        />
                      </button>
                    </>
                  }
                  disableUnderline
                  formclasses={{ root: classes.root }}
                  htmlFor="standard-input-user-password"
                  id="standard-input-user-password"
                  label="Password"
                  onChange={(e) => setDraftCustomer({ ...customerDraft, password: e.target.value })}
                  size="large"
                  type={isPasswordHidden ? 'password' : 'text'}
                  value={customerDraft.password || ''}
                  width="true"
                />
                <CButton
                  onClick={generatePassword}
                  type="primary"
                  xstyle={{
                    borderRadius: '12px',
                    margin: '16px 0px 0px 16px'
                  }}>
                  Generate
                </CButton>
              </div>
            )}
          </div>
        </form>
      </Modal>
    </div>
  );
};

export default CustomerDetailEditModal;

关键说明

  • 递归组件位置:ObjectFieldRenderer定义在父组件内部,可直接访问classes、restrictedData等变量,无需额外传参。
  • 嵌套更新逻辑:通过updateNested递归函数实现深层对象更新,确保修改metadata.birthDate这类嵌套字段时,不会破坏原对象结构。
  • 样式优化:给嵌套字段添加左侧缩进和标题区分层级,同时给Modal添加垂直滚动,避免内容溢出。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 19:30:54