如何在更新状态对象时缓存递归渲染的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,但修改输入时所有元素还是会重渲染,该怎么解决?最佳实现方案是什么?
解决方案
问题核心
当前的问题在于:
- 递归渲染的数组/对象容器组件没有被缓存,父组件重渲染时所有容器都会跟着刷新;
onValueChange函数每次重渲染都会生成新引用,导致MemoObjectRendererInput的props变化,触发不必要的重渲染;- 对象更新逻辑虽然做了顶层浅拷贝,但嵌套对象的引用未处理,配合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
相关产品推荐
相关产品推荐

