React useState hook explained

Published on: 1/11/2024

two sets of universes one on the left side enclosed inside a 3d hexagonal translucent box and a second one loose and scrambled all over the place without order, watercolor

Contents

  1. Few words about state in React
  2. useState API
    1. Initializing state and setting initial value
    2. Mutating state
    3. Naming state and setter function
    4. Overthrowing React state management
  3. Invoking state mutation functions
    1. Accessing new state value
    2. Changing current state based on previous (or pending) state
    3. Storing objects in state
  4. Conclusion

Few words about state in React

Changes to state in React cause a re-render, that means it cannot be changed directly if we want to see changes to it reflected on the screen. Changing state is done by using specialized functions that manipulate internal variables that React manages and updates them for next render.

There are two main hooks used for managing state (useContext is a bit different beast) in React: useState and useReducer hooks. Before hooks state was an object and it was slightly more similar to what reducer representation looks like, but we'll deal with the hooks only today.

useState API

From the two useState has much simpler API layer:

App.tsx
1import { useState } from 'react';
2
3function App() {
4 const [count, setCount] = useState(0);
5
6 // define functions that deal with state changes
7 const increment = () => setCount(count + 1);
8 const decrement = () => setCount(count - 1);
9
10 return (
11 <div className="flex flex-col bg-black text-white h-dvh w-dvw gap-4 text-2xl justify-center align-middle">
12 <div className="flex items-center justify-center gap-4">
13 <button
14 onClick={decrement}
15 className="text-orange-600 border-2 border-orange-800 aspect-square w-6 flex items-center justify-center"
16 >
17 <p className="-mt-1">-</p>
18 </button>
19 <button
20 onClick={increment}
21 className="text-teal-600 border-2 border-teal-900 aspect-square w-6 flex items-center justify-center"
22 >
23 <p className="-mt-1 p-0">+</p>
24 </button>
25 </div>
26 <div className="flex items-center justify-center gap-4 text-4xl">
27 <span className="text-emerald-700 underline underline-offset-4 decoration-emerald-900">
28 {count}
29 </span>
30 </div>
31 </div>
32 );
33}
34
35export default App;

Initializing state and setting initial value

In this case we are importing useState hook from react to allow a variable (or state) to change over time (between renders). We initialize our count state with value 0 - it allows us to control what the count variable is when the application starts.

When we invoke the useState function we can also set initial value for our variable. It is passed as a parameter to the useState function.

1const [count, setCount] = useState(0);

Here we have set the count variable to initial value of 0. Setting initial value is optional and you can also use a setter function that will be invoked when state is initialized and never again.

1const [count, setCount] = useState(computationallyExpensiveInitializationOfState);

Note that we are not invoking function with useState(computationallyExpensiveInitializationOfState) but are passing the function to the useState hook. This way React will invoke it only once.

If we did invoke it it would happen on every render of the component and could be slow down our application for no reason.

Mutating state

To change the state we defined two functions increment and decrement that manipulate (mutate or change) the count variable.

The variable and function can be called whatever you fancy but it is a common pattern to use name and setName when destructuring these values from useState hook.

Try changing the setCount function to forceCountToBe like so:

Naming state and setter function

1function App() {
2 const [count, forceCountToBe] = useState(0);
3
4 // define functions that deal with state changes
5 const increment = () => forceCountToBe(count + 1);
6 const decrement = () => forceCountToBe(count - 1);
7 //...
8}

It still works! But why do we need to use this special function?

Overthrowing React state management

Couldn't we just update the state directly? Let's try it. Change forceCountToBe back to setCount and replace the increment function with:

1() => count = count + 1;

and declare the destructured count and setCount with let instead of const.

1 let [count, setCount] = useState(0);

And a bit surprisingly it actually kind of works.. if you decrease the variable after incrementing it you will see that it was updated. This happens because React does not track the count variable changes itself but uses setCount function to schedule a re-render of the component with new value. And schedule is an important distinction here. Let's go back to the expected state of the world. Change the increment function back to: const increment = () => setCount(count + 1); and let us explore the useState hook some more.

Invoking state mutation functions

Maybe for some reason we need to increment the value twice... how can we do this?

Easy right? Invoke the function twice (that might not have been your first thought but bear with me) like so:

App.tsx
1import { useState } from 'react';
2
3function App() {
4 const [count, setCount] = useState(0);
5
6 // define functions that deal with state changes
7 const increment = () => setCount(count + 1);
8 const decrement = () => setCount(count - 1);
9
10 return (
11 <div className="flex flex-col bg-black text-white h-dvh w-dvw gap-4 text-2xl justify-center align-middle">
12 <div className="flex items-center justify-center gap-4">
13 <button
14 onClick={decrement}
15 className="text-orange-600 border-2 border-orange-800 aspect-square w-6 flex items-center justify-center"
16 >
17 <p className="-mt-1">-</p>
18 </button>
19 <button
20 onClick={() => {
21 increment();
22 increment();
23 }}
24 className="text-teal-600 border-2 border-teal-900 aspect-square w-6 flex items-center justify-center"
25 >
26 <p className="-mt-1 p-0">+</p>
27 </button>
28 </div>
29 <div className="flex items-center justify-center gap-4 text-4xl">
30 <span className="text-emerald-700 underline underline-offset-4 decoration-emerald-900">
31 {count}
32 </span>
33 </div>
34 </div>
35 );
36}
37
38export default App;

We are telling React that when button is clicked it should run a function that will increment the count twice:

App.tsx
1onClick={() => {
2 increment();
3 increment();
4}}

But it does not work, our count is still incremented by 1.

Let's check what happens and use good old console.log to check what the value is before and after each increment:

App.tsx
1onClick={() => {
2 console.log('count (before increment): ', count);
3 increment();
4 console.log('count (after first increment): ', count);
5 increment();
6 console.log('count (after second increment): ', count);
7}}

What you get in the console:

1count (before increment): 11
2count (after first increment): 11
3count (after second increment): 11

Now wait a second, you may say.. NOTHING changed even after the setCount was called!

This is what I meant that React schedules the updates. The function does not really increment value until next render. If you think about it in a broader scope it will become clear - what if we had 20 invocations to different set... functions? Should React re-render on every invocation then? You would only care if the whole application was ready and not about partial updates. And that is why React schedules updates instead of changing the value immediately.

This leads us to the next "quirk" as you might see it: how then are we supposed to access the value after the change but before the state is re-rendered? We may need it to compute other things and now it seems we need to run it after the page renders again?

Accessing new state value

The solution is quite simple...

If React itself is does not expect values of state to change within single frame we need to "escape" it or move aside and use it to our advantage. What if we just created a new variable that holds the new value and used that? Seems straightforward enough so let's see what happens.

App.tsx
1 const increment = () => { const newCount = count + 1; setCount(newCount) };
2 const decrement = () => { const newCount = count - 1; setCount(newCount) };

Now accessing new count value is easy:

App.tsx
1console.log('count (after increment): ', newCount);

Now suppose we now need to double the value of count only if it's divisible by 15. How would we access the previous value? Turns out that React can instead of new state accept an updater function that will take the currently pending state and return it allowing us to manipulate it's value.

Changing current state based on previous (or pending) state

To use the updater function for new state we pass it as an argument to the function in setCount:

App.tsx
1 const increment = () => setCount((count) => count % 15 === 0 ? count * 2 : count + 1);
2 const decrement = () => setCount((count) => count - 1);

If you are curious how that could affect our previous examples, here's a question: What would happen if we introduced such change to our increment function and invoked it multiple times?

App.tsx
1import { useState } from 'react';
2
3function App() {
4 const [count, setCount] = useState(0);
5
6 // define functions that deal with state changes
7const increment = (step=1) => setCount((prevCount) => prevCount + step);
8const decrement = (step=1) => setCount((prevCount) => prevCount - step);
9
10 return (
11 <div className="flex flex-col bg-black text-white h-dvh w-dvw gap-4 text-2xl justify-center align-middle">
12 <div className="flex items-center justify-center gap-4">
13 <button
14 onClick={decrement}
15 className="text-orange-600 border-2 border-orange-800 aspect-square w-6 flex items-center justify-center"
16 >
17 <p className="-mt-1">-</p>
18 </button>
19 <button
20 onClick={() => {
21 increment(); // 1
22 increment(); // 2
23 increment(); // 3
24 }
25 }
26 className="text-teal-600 border-2 border-teal-900 aspect-square w-6 flex items-center justify-center"
27 >
28 <p className="-mt-1 p-0">+</p>
29 </button>
30 </div>
31 <div className="flex items-center justify-center gap-4 text-4xl">
32 <span className="text-emerald-700 underline underline-offset-4 decoration-emerald-900">
33 {count}
34 </span>
35 </div>
36 </div>
37 );
38}
39
40export default App;

If you look at the result it will show '3' after the third invocation of the increment function. It is completely different from the previous example.

This is due to the fact that we are now using the updater function to access the pending or scheduled state. This has also added benefit that we do not need to use the current count variable to access the current state and our function is not dependant on it. It is very handy as when used with useEffect it will not need to add count to the dependency array. I am getting ahead of myself but I do find this part very important so keep in mind that using updater function helps limit the amount of variables we depend on for effects.

Storing objects in state

What if we have user data we want to store in state? Since we don't really want to separate data into multiple useState hooks like:

1const [firstName, setFirstName] = useState('');
2const [lastName, setLastName] = useState('');
3const [email, setEmail] = useState('');
4const [address, setAddress] = useState('');
5
6// etc..

We should use an object to do this:

1const [user, setUser] = useState({
2 firstName: '',
3 lastName: '',
4 email: '',
5 address: '',
6 status: '',
7 lastLogin: null,
8 blocked: false,
9});

If we need to update one of the properties we can do it like this:

1setUser((prevUserData) => ({
2 ...prevUserData,
3 firstName: 'John',
4}));

We are now recreating user object by spreading the prevUserData object and overwriting the firstName property with the new value and finally assigning it with setUser function.

Conclusion

Today we have learnt how useState hook can be used to manage state in React and how to:

  1. Initialize state with initial value or updater function.
  2. Change (mutate) current state and use variables to access new state before it updates.
  3. Change current state based on pending state.
  4. Store objects in state and update their data when one part is replaced.
Back to Articles

By continuing to use this site you consent to the use of cookies 🍪 in accordance with our 🍪 cookie policy. This message will display once every 70 days or when the policy changes.