I have a React component that renders a moderately large list of inputs (100+ items). It renders okay on my computer, but there's noticeable input lag on my phone. The React DevTools shows that the entire parent object is rerendering on every keypress.
Is there a more efficient way to approach this?
https://codepen.io/anon/pen/YMvoyy?editors=0011
function MyInput({obj, onChange}) {
return (
<div>
<label>
{obj.label}
<input type="text" value={obj.value} onChange={onChange} />
</label>
</div>
);
}
// Passed in from a parent component
const startingObjects =
new Array(100).fill(null).map((_, i) => ({label: i, value: 'value'}));
function App() {
const [objs, setObjects] = React.useState(startingObjects);
function handleChange(obj) {
return (event) => setObjects(objs.map((o) => {
if (o === obj) return {...obj, value: event.target.value}
return o;
}));
}
return (
<div>
{objs.map((obj) => <MyInput obj={obj} onChange={handleChange(obj)} />)}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
myArray. push(1); However, with React, we need to use the method returned from useState to update the array. We simply, use the update method (In our example it's setMyArray() ) to update the state with a new array that's created by combining the old array with the new element using JavaScript' Spread operator.
React do not update immediately, although it seems immediate at first glance.
The issue is related to:
function handleChange(obj) {
return (event) => setObjects(objs.map((o) => {
if (o === obj) return {...obj, value: event.target.value}
return o;
}));
}
In this, you will update the objs
array. This is obviously fine, but React doesn't know what has changed, so triggered Render on all the Children.
If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost.
https://reactjs.org/docs/react-api.html#reactmemo
const MyInput = React.memo(({obj, onChange}) => {
console.log(`Rerendered: ${obj.label}`);
return <div style={{display: 'flex'}}>
<label>{obj.label}</label>
<input type="text" value={obj.value} onChange={onChange} />
</div>;
}, (prevProps, nextProps) => prevProps.obj.label === nextProps.obj.label && prevProps.obj.value === nextProps.obj.value);
However, React.Memo only does a shallow comparison when trying to figure out if it should render, so we can pass a custom comparison function as the second argument.
(prevProps, nextProps) => prevProps.obj.label === nextProps.obj.label && prevProps.obj.value === nextProps.obj.value);
Basically saying, if the label and the value, on the obj
prop, are the same as the previous attributes on the previous obj
prop, don't rerender.
Finally, setObjects
much like setState, is also asynchronous, and will not immediately reflect and update. So to avoid the risk of objs
being incorrect, and using the older values, you can change this to a callback like so:
function handleChange(obj) {
return (event) => {
const value = event.target.value;
setObjects(prevObjs => (prevObjs.map((o) => {
if (o === obj) return {...obj, value }
return o;
})))
};
}
https://codepen.io/anon/pen/QPBLwy?editors=0011 has all this, as well as console.logs, showing if something rerendered.
Is there a more efficient way to approach this?
You are storing all your values in an array, which means you don't know which element needs to be updated without iterating through the whole array, comparing if the object matches.
If you started with an Object:
const startingObjects =
new Array(100).fill(null).reduce((objects, _, index) => ({...objects, [index]: {value: 'value', label: index}}), {})
After some modifications, your handle function would change to
function handleChange(obj, index) {
return (event) => {
const value = event.target.value;
setObjects(prevObjs => ({...prevObjs, [index]: {...obj, value}}));
}
}
https://codepen.io/anon/pen/LvBPEB?editors=0011 as an example of this.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With