~ 4 min

Managing an array with useState with performance in mind

I've recently wrote about Managing an array with useState. That article showed a rather simple example about managing an array with useState. Despite illustrating the point quite well, someone made me notice that it did not take performance into account: as is, the code does a bunch of unnecessary re-renders.

If you're new to react, I'm sure you'll soon discover that when a project starts to gain a certain size, performance starts being an issue, and some optimization is needed. In this post, I'll take the code from the previous article and clean/refactor it to avoid unnecessary re-renders.

Old Code

Here's the "old" code shown last time.

import { useState } from 'react'

const Numbers = ({ numbers, onRemove }) => (
<div>
{numbers.map(value => {
return (
<div key={value}>
{value} <button onClick={() => onRemove(value)}>-</button>
</div>
)
})}
</div>
)

const App = () => {
const [arr, setArray] = useState([0, 1, 2, 3, 4, 5])

const appendToArray = nextElement => {
const next_arr = [...arr, nextElement]
setArray(next_arr)
}
const removeIndexFromArray = index => {
const next_arr = [...arr.slice(0, index), ...arr.slice(index + 1)]
setArray(next_arr)
}

//
const addNext = () => appendToArray(arr.length)
const onRemoveIndex = idx => removeIndexFromArray(idx)

return (
<div>
<Numbers numbers={arr} onRemove={onRemoveIndex} />
<button onClick={addNext}>+</button>
</div>
)
}

There are a couple of situations we need to look into to make it better.

Doing some cleaning first

As you can see, I originally defined appendToArray and removeIndexFromArray inside the body of the component. To clean this and avoid cluttering the body of the component, we can move those out and make the code generic. We can achieve this by passing arr as an argument to the functions and removing the setArray dependency. This way, we're left with two small functions whose sole purpose is to add and remove an element from an array. As such, we end up with this:

const appendToArray = (arr, nextElement) => [...arr, nextElement]

const removeIndexFromArray = (arr, index) => [
...arr.slice(0, index),
...arr.slice(index + 1),
]

And those will be called later like this:

const addNext = () => setArray(appendToArray(arr, arr.length))

const onRemoveIndex = idx => setArray(removeIndexFromArray(arr, idx))

Notice how we simply return an array from the functions, passing them to setArray that we purposely left out of the body of those helper functions.

First problem: Callbacks

One of the reasons a component might re-render is when one or more of its props change. Sadly, every time our component re-renders it re-defines addNext and onRemoveIndex and passing them to children, triggers re-renders. One way to prevent this is to use the useCallback hook. It will memoize your functions and only change them if one of their dependencies changes. Doing is is straight forward:

const addNext = useCallback(() => setArray(appendToArray(arr, arr.length)), [
setArray,
arr,
])

const onRemoveIndex = useCallback(
idx => setArray(removeIndexFromArray(arr, idx)),
[setArray, arr]
)

If you are wondering what is the array passed as the second argument to useCallback, it's the list of dependencies needed inside. List them all, and you should be good to do. By doing this, addNext and onRemoveIndex will only change if arr or setArray change and so, only when they change, our children will re-render.

Second problem: Parent renderings

Another reason a component might re-render is if its parent re-renders, even if the received props are the same. This is also an easy fix: once you identify the component you know should not be re-rendering, wrap it with memo:

import { memo } from 'react'

const Component = memo(() => {
// ...
})

Complete example

We got to the point where I can show you the final code. You'll notice a couple more changes I did, mostly to test the re-renderings:

  • The Numbers component has been split into three components: Numbers, Number and Digit;
  • The main logic regarding rendering the array has been move into a separate component called Main that is wrapped with memo;
  • I added a local state v to the App component. It allows me to change it with the click on the button and verify what re-renders. This makes testing very easy: when you click the button and change v, the new value of v should be re-rendered, but the list of numbers should not.

Here's the code:

import React, { useCallback, useState, memo } from 'react'

// Helpers
const appendToArray = (arr, nextElement) => [...arr, nextElement]
const removeIndexFromArray = (arr, index) => [
...arr.slice(0, index),
...arr.slice(index + 1),
]

// Components
const Digit = ({ value }) => <span>{value}</span>

const Number = ({ value, onRemove }) => (
<div>
<Digit value={value} /> <button onClick={() => onRemove(value)}>-</button>
</div>
)

const Numbers = ({ numbers, onRemove }) =>
numbers.map(value => (
<Number key={value} value={value} onRemove={onRemove}></Number>
))

const Main = memo(() => {
// State
const [arr, setArray] = useState([0, 1, 2, 3, 4, 5])

// Callbacks
const addNext = useCallback(() => setArray(appendToArray(arr, arr.length)), [
setArray,
arr,
])

const onRemoveIndex = useCallback(
idx => setArray(removeIndexFromArray(arr, idx)),
[setArray, arr]
)

return (
<div>
<Numbers numbers={arr} onRemove={onRemoveIndex} />
<button onClick={addNext}>+</button>
</div>
)
})

// Main
const App = () => {
const [v, setV] = useState(0)

return (
<div>
{v}
<button onClick={() => setV(v + 1)}>click me</button>
<Main />
</div>
)
}

export default App

If you click on the "click me" button, only the display of v will re-render, but if you had or remove an element from the array, then they will re-render too.

Final notes

I hope you learned a thing or two about the optimization of re-renderings. If you did, I suggest you go through your own code and look for situations that might need similar fixes. To easily spot those situations, use the profiler provided with the react dev tools extension. I'll write about this in a future article, so if you do not want to miss it, subscribe to my newsletter.

Subscribe to the newsletter

As a full-stack web developer I write about both the backend and frontend. If you liked what you read and want more, subscribe to my newsletter and I'll be sure to let you know once I release new articles.

I hope you like it! Share it with others who could enjoy it too.

Related posts

If you liked this post and want to read more, take a loot at these: