TL;DRTo avoid having noticeable lag between re-renders, we should identify our expensive computations and pre-compute them before they reach our components.
Re-renders in React isn’t the devil. But it can be one when we dump all of our business logic there, including the most trivial data transformations.
The problem
Let’s examine a common scenario.
We get the backend response, an array of objects.
const data = await fetch('/api/books').then((res) => res.json());!
We map over this array in order to render a table
<Table.Container>
<Table.Header>
<Table.HeaderCell>...</Table.HeaderCell>
<Table.HeaderCell>...</Table.HeaderCell>
<Table.HeaderCell>...</Table.HeaderCell>
</Table.Header>
{data.map((book) => (
<Table.Row key={book.uuid}>
<Table.Cell>
<ComponentA propA={...book.propertyA} propB={...book.propertyB} />
</Table.Cell>
<Table.Cell>
<ComponentB propC={...book.propertyC} />
</Table.Cell>
<Table.Cell>
<ComponentC propA={...book.propertyA} propB={...book.propertyB} />
</Table.Cell>
</Table.Row>
))}
</Table.Container>
Each row of the table is a component that holds multiple sub-components
<Table.Cell>
<ComponentA {...book.propertyA} />
</Table.Cell>

There shouldn’t be an issue, right? Our content changes only by applying filters, pagination, or sorting. All will trigger a fresh batch of data, so there won’t be a noticeable change.
But, what if we implement a functionality where we can select a range of items? For example, we can pick the first ten items, the last ten items, or the whole page. Here’s where things get tricky.
<Table.Container>
<Table.Header>
<Table.HeaderCell>
<Checkbox
state={tableSelectionState} // 'ALL' | 'SOME' | 'NONE'
onChange={handleAllSelection}
/>
</Table.HeaderCell>
<Table.HeaderCell>...</Table.HeaderCell>
<Table.HeaderCell>...</Table.HeaderCell>
<Table.HeaderCell>...</Table.HeaderCell>
</Table.Header>
{data.map((book) => (
<Table.Row key={book.uuid}>
<Table.Cell>
<Checkbox
isChecked={selectedBooks.includes(book.uuid)}
onChange={handleCheckboxSelect}
/>
</Table.Cell>
<Table.Cell>
<ComponentA propA={...book.propertyA} propB={...book.propertyB} />
</Table.Cell>
<Table.Cell>
<ComponentB propC={...book.propertyC} />
</Table.Cell>
<Table.Cell>
<ComponentC propA={...book.propertyA} propB={...book.propertyB} />
</Table.Cell>
</Table.Row>
))}
</Table.Container>

By clicking the 'select-all' checkbox in the header row, all the components will re-render. This is a very expensive operation.
The same applies for selecting a single row. The parent that holds the state will update and the rest of the components will re-render.
Oh no, it’s slow. It has always been.
No point thinking about these pesky re-renders. Our components were slow from the start.
Extracting expensive computations
We should dig deep into our components and look for the expensive computations.
Commonly the culprits are:
- Date parsing/formatting
- Nested loops
By formatting our data the moment we get them from the backend, (adding an adapter if you may), we guarantee two things:
- All the expensive computations are done on the get go, are co-located, easier tested and probably cached.
- Future incompatibilities with the backend will be fixed in a single place
Final thoughts
That pretty much sums it up. Before going crazy about multiple rerenders, memoizing everything, we should take a step back and figure out if our components are doing too much.
For more digging on composition, re-renders and et al, I suggest following up on this great read: The mystery of React Element, children, parents and re-renders
Resources