Reducing React table rerendering

There were some things that were making the row rerender:

First, the row component needed to be wrapped in React.memo

const ThingRow = React.memo(({ thing, onUpdate }) => {
   ...
})

Then the onUpdate needed to be memoized as well with React.useCallback, and to reduce the dependencies in useCallback to only setThings(see note), you can use the callback version of setThings and .map to update the item

const ListOfThings = ({ things, setThings }) => {
  const onUpdate = React.useCallback(
    (thing) => {
      setThings((prevThings) =>
        prevThings.map((t) => (t.id === thing.id ? thing : t))
      );
    },
    [setThings]
  );
  return (
    <table>
      <tbody>
        {things.map((thing) => (
          <ThingRow key={thing.id} thing={thing} onUpdate={onUpdate} />
        ))}
      </tbody>
    </table>
  );
};

When having this kind of issue you need to look for the way to memoize props you are passing down, especially callbacks, and reduce dependencies in hooks

this is the codesandbox fork https://codesandbox.io/s/table-row-memoization-xs1d2?file=/src/App.js

note: according react docs

React guarantees that setState function identity is stable and won’t change on re-renders


There are a couple of optimizations you can try. First of all, if you are dealing with a lot of data, the core problem could be that you are rendering too many things to the DOM. If this is the case, I would start by adding virtualization or pagination to the table. You could easily add virtualization with a library, e.g. using FixedSizeList from react-window:

import { FixedSizeList as List } from 'react-window';
 
const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);
 
const Example = () => (
  <List
    height={150}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);

This should already cut down on any performance problems, but if you only want to optimize rerenders: The default behavior for react is to render the current component where a useState setter was called, and all child components, even without any props changing. so you should wrap both ListOfThings and ThingRow in memo to opt out of this behavior:

const ListOfThings = memo({ things, setThings }) => {
   ...
});
const ThingRow = memo(({ thing, onUpdate }) => {
   ...
});

If you don't do this, changing the unrelated title input will lead to rerendering everything.

Lastly, you are passing an inline event handler (onUpdate) to ThingRow. This will create a new function object for each rendering of ListOfThings, which would bypass the memo optimization we did earlier. To optimize this, you could create a persistent reference to the function with useCallback:

onUpdate={useCallback(
  thing => setThings(prev => prev.map(t => t.id === thing.id ? thing : t)),
  [setThings])}

Note that it's considered best to use the function update style in useState whenever you are relying on a previous value for the update.

Small disclaimer: If you are having performance problems, I would go only with the virtualization / pagination solution, and not try at all to optimize for rerenders. React's default rendering behavior is very fast, and changing it could potentially introduce hard to find bugs related to things not updating.