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

如何在更新状态对象时缓存递归渲染的React元素

问题

我用递归渲染的方式展示对象结构,每个值对应一个输入框,键对应标签。修改输入框值时,会更新对象并触发重渲染,但每次重渲染都会刷新所有元素,UI有明显延迟。我想缓存元素,只重渲染被修改的输入框,但因为用扩展运算符触发重渲染,一直没搞定。

示例对象:

{
  "id": "12345",
  "details": {
    "name": "jake"
  },
  "attachments": [
    {"file": "spodjgposg.file"},
    {"file": "sdgidfhoew.file"}
  ]
}

渲染组件代码:

// all imports

const MemoObjectRendererInput = React.memo(ObjectRendererInput);

type Data = Record<any, any>;

type Entry = string | boolean | unknown[] | Data;

type Props = {
  onDataChange: (data: Data) => void;
  data: Data;
};

export const ObjectRenderer: FC<Props> = props => {
  const {data, onDataChange} = props;

  const onValueChange = (value: string, path: string) => {
    const _data = {...data};
    updateByPath(_data, path, value); // 更新对象的函数
    onDataChange(_data);
  };

  const render = (entry: Entry, label = '', level = -1, parent = 'root') => {
    if (typeof entry === 'string' || typeof entry === 'boolean') {
      const path = parent.replace('root.', '');
      return (
        <MemoObjectRendererInput
          onValueChange={v => onValueChange(v, path)}
          label={label}
          value={entry}
        />
      );
    }

    if (Array.isArray(entry)) {
      return (
        <div>
          <ObjectRendererLabel />
          {entry.map((item, index) => {
            return render(item, index.toString(), level + 1);
          })}
        </div>
      );
    }

    if (typeof entry === 'object') {
      return (
        <div>
          <ObjectRendererLabel />
          {isExpanded && Object.entries(entry).map(([key, value]) => {
            return render(value, key, level + 1, [parent, key].join('.'));
          })}
        </div>
      );
    }
  };

  return (
    <div>
      {render(data)}
    </div>
  );
};

updateByPath函数代码:

const updateByPath = (object: Record<any, any>, path: string, value: string | boolean) => {
  const _path = path.split('.');

  while (_path.length > 1) {
    const key = _path.shift();
    if (key) object = object[key];
  }

  const key = _path.shift();
  if (key) object[key] = value;
};

我试过缓存ObjectRendererInput,也给render函数加了useCallback,但修改输入时所有元素还是会重渲染,该怎么解决?最佳实现方案是什么?


解决方案

问题核心

当前的问题在于:

  1. 递归渲染的数组/对象容器组件没有被缓存,父组件重渲染时所有容器都会跟着刷新;
  2. onValueChange函数每次重渲染都会生成新引用,导致MemoObjectRendererInput的props变化,触发不必要的重渲染;
  3. 对象更新逻辑虽然做了顶层浅拷贝,但嵌套对象的引用未处理,配合memo组件的缓存逻辑不够严谨。

分步优化方案

1. 拆分递归逻辑为独立缓存组件

把数组、对象、单个条目的渲染逻辑拆成独立组件,用React.memo包裹,避免父组件重渲染时这些容器跟着刷新:

// 数组渲染容器
const ArrayRenderer = React.memo(({ entry, parent, onValueChange }: {
  entry: Entry[];
  parent: string;
  onValueChange: (value: string | boolean, path: string) => void;
}) => {
  return (
    <div>
      <ObjectRendererLabel />
      {entry.map((item, index) => (
        <ObjectRendererEntry
          key={index} // 优先用数组项的唯一ID(如item.id)代替index
          entry={item}
          label={index.toString()}
          parent={`${parent}.${index}`}
          onValueChange={onValueChange}
        />
      ))}
    </div>
  );
});

// 对象渲染容器
const ObjectContainer = React.memo(({ entry, parent, onValueChange }: {
  entry: Data;
  parent: string;
  onValueChange: (value: string | boolean, path: string) => void;
}) => {
  return (
    <div>
      <ObjectRendererLabel />
      {isExpanded && Object.entries(entry).map(([key, value]) => (
        <ObjectRendererEntry
          key={key}
          entry={value}
          label={key}
          parent={`${parent}.${key}`}
          onValueChange={onValueChange}
        />
      ))}
    </div>
  );
});

// 单个条目处理组件
const ObjectRendererEntry = React.memo(({ entry, label, parent, onValueChange }: {
  entry: Entry;
  label: string;
  parent: string;
  onValueChange: (value: string | boolean, path: string) => void;
}) => {
  // 稳定输入框的change回调
  const handleInputChange = React.useCallback((v: string | boolean) => {
    const path = parent.replace('root.', '');
    onValueChange(v, path);
  }, [onValueChange, parent]);

  if (typeof entry === 'string' || typeof entry === 'boolean') {
    return (
      <MemoObjectRendererInput
        onValueChange={handleInputChange}
        label={label}
        value={entry}
      />
    );
  }
  if (Array.isArray(entry)) {
    return <ArrayRenderer entry={entry} parent={parent} onValueChange={onValueChange} />;
  }
  if (typeof entry === 'object' && entry !== null) {
    return <ObjectContainer entry={entry} parent={parent} onValueChange={onValueChange} />;
  }
  return null;
});

2. 稳定父组件的回调函数

在ObjectRenderer中用useCallback包裹onValueChange,确保函数引用不会每次重渲染都变化:

export const ObjectRenderer: FC<Props> = props => {
  const {data, onDataChange} = props;

  // 稳定onValueChange引用
  const onValueChange = React.useCallback((value: string | boolean, path: string) => {
    const _data = {...data};
    updateByPath(_data, path, value);
    onDataChange(_data);
  }, [data, onDataChange]);

  return (
    <div>
      <ObjectRendererEntry
        entry={data}
        label=""
        parent="root"
        onValueChange={onValueChange}
      />
    </div>
  );
};

3. 优化对象更新逻辑(可选)

用immer库简化不可变更新,确保只有修改路径上的对象引用变化,进一步提升缓存效率:

import { produce } from 'immer';

// 替换原onValueChange
const onValueChange = React.useCallback((value: string | boolean, path: string) => {
  const updatedData = produce(data, draft => {
    updateByPath(draft, path, value); // 直接修改draft,immer自动生成新对象
  });
  onDataChange(updatedData);
}, [data, onDataChange]);

4. 优化数组key

数组渲染时,优先使用数组项的唯一标识(如item.id)作为key,避免数组项顺序变化时触发不必要的重渲染。

最终效果

修改某个输入框时,只有该输入框所在路径上的组件会重渲染,其他未修改的部分会被React.memo缓存,大幅降低UI延迟。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 04:20:21