I’m rendering elements that reflects the structure of an object, so each value is an input, each key is a label and the render function is recursive. When changing the value of an input I update the object with the new value and re-render. The problem is that each re-render is rendering all the elements and it is noticeable in the UI (in delay). I want to memoize the elements so it re-render only the changed input but I am having trouble doing so since I am using a spread operator in order to force the re-render after updating the object.
Let’s take an object for example:
{
"id": "12345",
"details": {
"name": "jake",
},
"attachments": [
{"file": "spodjgposg.file"},
{"file": "sdgidfhoew.file"},
],
}
And the component to render the elements base on the object:
// 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); // the function to update the object
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>
);
};
This is the updateByPath
function:
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;
};
I tried memoize ObjectRendererInput
also tried to use useCallback
on the render
function but when changing a value on an input all the elements are re-rendered.
How can I solve this? What is the best implementation to achieve this?