Full-stack Web Technologies

CHAPTER 3
useState

useState makes React keep a piece of state for us.

  • The first time it is called, we get to decide what is the initial value. React knows if it is the first time because the piece of state is just missing.

  • On the subsequent calls, React just returns the stored state.

Calling useState

useState has 1 parameter which is the initial value (which is only really used in the first call). Then it returns two things:

  1. the stored value, and
  2. a function to change it.

To be able to return two things, it returns an array with two cells. If we wrote the code normally, it would look like this:

const result = useState(0);
const value = result[0];
const setValue = result[1];

But since Javascript has destructuring, typically we will write it like this, which makes it easy to use and we get to decide what are the names given to the value and the function:

const [value, setValue] = useState(0);

A Counter

A classical first client component is a counter, which, using useState, is implemented like this:

export default function Counter() {
  const [count, setCount] = useState(0);

  const incr = () => setCount((c) => c + 1);

  return (
    <div>
      <div>{count}</div>
      <button onClick={incr}>Increment</button>
    </div>
  );
}

We have the call to useState, creating a piece of state with an initial value of 0. Then we define incr, which is a function that will increment the state by one unit (more on the setCount call later). And finally the JSX is returned, in which we have set the onClick attribute of a button to call incr.

Changing the state

To change the state we have to call the function that was given to us by useState. This function can be called in two ways:

The function, though, is not like an assignment. A call such as:

setCount(value)

will "schedule" two things: a) a change in state, b) a re-render. Both this things will happen after the current task is done. So they are events put on the event queue after the current one (therefore, like an asynchronous function).

Passing a literal

Passing a literal like in

setCount(5);

will schedule a state change to 5. The same state change can be done a number of times:

setCount(5);
setCount(5);

and it will not change the result, since programming a change to 5 two times is the same as doing it once.

Passing an expression

A similar thing happens if we do:

const [count, setCount] = useState(0);
// ...
const incr = () => setCount(count + 1);
// ...
return <button onClick={incr} />

but this time, you can get confused by thinking that setCount(count + 1) increments the counter like an assignment and think that, if we change incr to:

const incr = () => {
  setCount(count + 1);
  setCount(count + 1);
}

the counter will be incremented by 2. That is not the case!

Reinterpreting, setCount(count + 1) still says "program a change of state for later with a value of count + 1". Since count + 1 will always be the same value, you can do as many changes as you want, and it will still increment by 1.

Passing a function

The solution for those cases is passing a function:

setCount(prev => prev + 1);

If setCount sees a function, it will apply the function to whatever the value for the counter is at that time and it will update it, so something like:

setCount(prev => prev + 1);
setCount(x => x + 1);

will increment the counter by 2, since every time we are using the latest value of the count to compute the new one. Also, as you can see, the name of the parameter of the arrow function passed to setState does not matter, albeit people usually call it prev meaning "previous state".