Array State Variables in React

by Sam A. Hill

Simple State Variables in React

In React.js, web sites are built of components which update automatically whenever certain variables, called state variables, change. In the "functional component" paradigm, a state variable called count might be defined in a component as

[count,setCount] = useState(0)

We can access the value of count directly, but it is a read-only variable; if we want to change it, we have to use the setCount function. So for instance, if we want to increment count by 1, we can't use

count++ //Won't work because count is read-only

but instead must use

setCount(count+1)

Using arrays as state variables

State variables can be any type we want, but things get a little trickier when we work with arrays (or dictionaries, or just objects in general) as state variables. Suppose we define a state variable to contain a list of names:

[names, setNames] = useState(["Alice","Bob","Charlie"])

How do we change the second name from "Bob" to "Bill"?

The naive approaches

The naive approach,

names[1] = "Bill"

won't work, because names is a read-only variable; we can only change it using the function setNames, and setNames is expecting to receive the entire list at once. So what we need to do is to move the array into a new variable, edit it, and then put it back into names.

Here was my first attempt to do this, back in the day.

let temp = names;
temp[1] = "Bill"
setNames(temp)

But this doesn't work! Why not?

Assignment by Reference

It comes down to a subtlety about lists in Javascript, one which has probably tripped up many a programmer. When we assign one list to another, like temp = names, we are assigning the array to temp by reference, not by value. In other words, we are giving the array names the "nickname" temp; wherever we would use names in the code, we can now substitute the variable temp instead, and vice versa. So that second line, temp[1]="Bill"? That means the same thing as names[1]="Bill", and we've already established that this doesn't work!

The solution

Cloning

What we need to do is to create a copy, or a clone of the names array to put into temp. There are numerous ways to do this, but in ES6 (i.e. "modern" Javascript) the easiest way is with the spread operator ...:

let temp = [...names];
temp[1] = "Bill"
setNames(temp)

…and that will do the trick.

Using hooks to simplify this

If you work a lot with state arrays, this might become rather tedious and it would be nice to package it up. To do that, we can write a custom hook like this one:

function useArrayState(initial) {
    const [list,setList] = useState(initial);
    const setElement = (index,value) => {
        let temp = [...list];
        tmp[index] = value;
        setList(temp);
    }
    return [list, setElement];
}

This will act as a dropin replacement for useState, only instead of returning a function that replaces the whole list at once, it returns the function setElement which only changes the list at the position index. Notice that the hook returns a state variable which is returned by the useState call in the first line.

Example

So for example, we could write

[names,setName] = useArrayState(["Alice","Bob","Charlie"])
setName(1,"Bill")

Hooks can return any number of functions, not just one, so if you miss having the ability to change the entire list at once, you can change the hook to be

function useArrayState(initial) {
    const [list,setList] = useState(initial);
    const setElement = (index,value) => {
        let temp = [...list];
        temp[index] = value;
        setList(temp);
    }
    return [list, setList, setElement];
}
//...
[names,setNames,setName] = useArrayState(["Alice","Bob","Charlie"])

If we replace [...list] with {...list}, this hook would work with simple objects (i.e. dictionaries) instead.