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)
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 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?
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!
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.
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.
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.