A Complete Guide to Managing Arrays with React's useState Hook

A Complete Guide to Managing Arrays with React's useState Hook

·

5 min read

The introduction of hooks made state management much easier in React.

With the useState hook, you're given a tuple where the first element is the current state and the second element is a function used to update that state. For primitive types like strings, using this hook to manage the state is straightforward.

But what about complex data types, like arrays?

The main thing to remember when working with arrays in React is that you don't want to mutate the array. React can't detect the change, so your UI won't update how you expect it to:

In this example, each button is supposed to double the value shown. But when you click the Mutate Array button, nothing seems to happen. This is because we're mutating the state and then calling setList with the same array reference. Because the reference is the same, React thinks the array hasn't changed and bails out of the re-render.

This is an optimization that React does internally where if you pass the same state as the current state to the updater function, React ignores the update.

Note: If you click the Mutate Button a few times and then click the Update Button, you'll notice that the value more than doubles. This is because both buttons reference the same list variable.

Technically the Mutate Button does update the value, React just doesn't know about it until the Update Button is clicked and setList is called with a new array reference.

So how do you handle this?

When updating arrays in React state, create a new array.

Instead of mutating the existing array, use it as the starting point to create a new array.

Let's take a look at a few examples:

How to add an element to an array

function ListOfNumbers() {
    const [list, setList] = useState([1]);
    return (
        <div>
            {/**
              * It's not a good practice to use the index
              * as the key, but hey this is just an example.
              */}
            {list.map((item, index) => <span key={index}>{item}</span>)}
            <button onClick={() => setList([...list, Math.random()])}>
                Add Item
            </button>
        </div>
    );
}

In this example, when you click the button element, we:

  1. Create a new list

  2. Spread the existing list into the new list

  3. Append a random number to the end of the list

The spread operator is a quick way to create a new array using the values of an existing array.

How to remove an element from a list

function ListOfNumbers() {
    const [list, setList] = useState([1, 2, 3, 4]);
    return (
        <div>
            {list.map((item, index) => <span key={index}>{item}</span>)}
            <button onClick={() => setList(list.slice(1))}>
                Remove First Item
            </button>
        </div>
    );
}

This time when the button is clicked, we slice off the first element. The slice array method returns a copy of the original array, which makes it safe to use with React state.

Note: On the other hand, the splice array method mutates the existing array and is therefore not a good fit for React state management.

How to update elements in an array

Let's say we have a list of numbers and want to double each array element when you click a button. In this case, we can use the map function to handle this:

function ListOfNumbers() {
    const [list, setList] = useState([1, 2, 3, 4]);
    return (
        <div>
            {list.map((item, index) => <span key={index}>{item}</span>)}
            <button onClick={() => setList(list.map(item => item * 2))}>
                Double Items
            </button>
        </div>
    );
}

What if we wanted to update a specific array element? Luckily you can use the map function for this too!

function ListOfNumbers() {
    const [list, setList] = useState([1, 2, 3, 4]);
    return (
        <div>
            {list.map((item, index) => <span key={index}>{item}</span>)}
            <button onClick={() => {
                setList(list.map(
                    (item, index) => index === 2 ? item * 2 : item
                ));
            }}>
                Double Items
            </button>
        </div>
    );
}

Notice how we're checking the index in our map function and we only double the item if it's the 3rd element (arrays start with the index 0).

How to safely mutate an array

Let's say you're trying to update your array and the easiest option to solve your problem is to reach for a built-in utility that mutates your array. For example, maybe you want to reverse the order of your array using the reverse array method. Since I said don't mutate your arrays, what should you do?

Make a copy of your array first!

The easiest thing to do is to use the spread operator to create a new copy of the array first. After you make the copy, you're free to mutate it however you want:

function ListOfNumbers() {
    const [list, setList] = useState([1, 2, 3, 4]);
    return (
        <div>
            {list.map((item, index) => <span key={index}>{item}</span>)}
            <button onClick={() => {
                const listCopy = [...list];
                listCopy.reverse(); // It's safe to mutate the copy
                setList(listCopy);
            }}>
                Reverse Items
            </button>
        </div>
    );
}

While the approach above will work in most cases, there's one major flaw.

The spread operator only creates a shallow clone of the original array. In other words, if you have an array of objects, each object within the cloned array will still reference the same object as the original array.

So how do you update objects within an array?

You do the same thing you did to update the array. You make a copy of the object you want to update using the spread operator:

function ListOfPeople() {
    const [list, setList] = useState([
        { id: 1, name: 'Jack' },
        { id: 2, name: 'Jill' }
    ]);

    return (
        <div>
            {list.map((item) => <span key={item.id}>{item.name}</span>)}
            <button onClick={() => {
                // This creates a new object for each item,
                // copies the old object, and updates the name.
                setList(list.map(item => ({
                    ...item,
                    name: item.name.toUpperCase()
                })));
            }}>
                Capitalize Names
            </button>
        </div>
    );
}

Awesome!

Now let's put all that together and take a look at a live running example: