Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React efficiently update object in array with useState hook

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);
like image 809
spiffytech Avatar asked Apr 21 '19 23:04

spiffytech


People also ask

How do you update an array of objects in React using useState?

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.

Does React useState hook update immediately?

React do not update immediately, although it seems immediate at first glance.


1 Answers

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.

like image 58
user2340824 Avatar answered Oct 25 '22 03:10

user2340824