React useState Hook
The useState hook allows a functional component to maintain it’s own state. A state change in a component results in a re-render of the component and all it’s children.
An example
By way of an example, let us look at a component which needs to keep track of the number of times the user have clicked a button. The count starts at zero and every time the user clicks an “increment” button the count increments by one.
info: I am assuming that you know how to create a new React project and get a React component to display on screen.
A naive solution
Before coming to the real solution using the useState hook, let us make a naive attempt to solve the problem without it. This will help us get a better understanding of useState.
1const Counter = () => {
2 let count = 0;
3 const handleIncrementClick = () => {
4 count = count + 1;
5 console.log(count);
6 };
7
8 return (
9 <>
10 <p>count is {count}</p>
11 <button onClick={handleIncrementClick}>increment</button>
12 </>
13 );
14};
15
16export default Counter;
Here the count
variable is initialized to 0 and we have hooked up the handleIncrementClick
function to the button’s onClick
event.
Try clicking on the increment
button; the count value shown on the page will stay at 0. Checkout the output of console.log
in your browser’s developer tools. You will see that the value of count variable is incrementing as expected. Let us take a closer look at what is going on here.
Why doesn’t our naive solution work?
React converts the JSX returned by the Counter
function to a JavaScript object. React then displays the UI described by this object on the screen. The handleIncrementClick
function becomes the onclick
handler for the HTML button element. Because of this, the handleIncrementClick
function and the count
variable used inside it, will not be garbage collected, even though the Counter
function has finished executing. But changes to the count
variable will not be visible on the screen.
We are using the count
variable inside the JSX like so:-
1<p>count is {count}</p>
We could have placed any JavaScript expression inside the curly braces, for example, {count * 2}
or {getCount()}
. This expression is evaluated before the function returns and it’s value becomes part of the JSX. React does not keep track of where this value comes from. It does not make an automatic link between the count variable and the value inside the JSX. It would be uncharacteristic of React to attempt something like this. It is not practical anyway, since the value inside the curly braces can be any JavaScript expression, not just a plain variable.
In short, our Counter
component works more or less like any normal JavaScript function. React does a little bit of magic behind the scenes when it comes to converting JSX to a JavaScript object, but other than that it doesn’t do anything on its own volition. If we want React to do something, we have to explicitly tell it to do it.
Using the useState hook
In this section we will learn how to use the useState hook. First, let us assess what we need from React to get our counter functionality working. We want React to call the Counter
function again when the value of count
changes and update the DOM based on the JSX returned. This is known in React as re-rendering a component. We also need a way to access the updated value of count inside the function. The useState hook provides both functionalities.
The useState hook is a function that accepts an (optional) value/expression as argument and returns an array containing two elements. The first element is the current value of the state variable. On first render, the current value is same as the initial value or null if we don’t pass an initial value. The second element is a setter function that can be used to update state and trigger a re-render. An example should make this clear:-
1import { useState } from "react";
2const [count, setCount] = useState(0);
First, we have to import the useState function from React. We pass 0 as the initial value for count
. useState returns the aforementioned array. We use JavaScript’s array destructuring assignment feature to unpack the values of the array into the variables count
and setCount
.
Full working example
1import { useState } from "react";
2
3const Counter = () => {
4 const [count, setCount] = useState(0);
5 const handleIncrementClick = () => {
6 setCount(count + 1);
7 };
8
9 return (
10 <>
11 <p>count is {count}</p>
12 <button onClick={handleIncrementClick}>increment</button>
13 </>
14 );
15};
16export default Counter;
Explanation
To get our Counter component working, we put React in charge of managing state. The initial value of count
(0 in our case) is stored by React internally. useState also returns the initial value back, plus a function that we can use to change the React state. So by this point there are two copies of count
. One which is stored by React and the one returned by useState and available for use in our function.
In handleIncrementClick
, we call the setCount
function. This results in an update to the value of count
stored by React and a re-render. During the re-render, useState
returns the updated value of count
. Note that, the initial value we pass to useState is used only on the first render, it is ignored on re-renders.
An important restriction
The useState hook must be called at the top level of a component or inside a custom hook.
info: Custom hooks are not discussed in this article.
So we must not call the hook inside a conditional statement or a loop. This restriction is applicable to all hooks, not just the useState hook.
Why such a restriction?
The short answer is that React uses call order (the order in which we call the useState hook in the component) to align the right-hand side of the hook with it’s left-hand side correctly on each render.
info: When I was first starting with React hooks, I felt that there was too much magic going on behind the scenes and I found it very confusing. Learning about the importance of call order, demystified React hooks for me. That is why I decided to discuss this topic in some detail. If you already understand React hook’s reliance on call order, feel free to skip this section.
As you would expect, we can call the useState hook multiple times in a component.
1const [name, setName] = useState("John Doe");
2const [age, setAge] = useState(35);
Under the hood of the useState hook, React is providing us with functionalities similar to that of a JavaScript map.
info: A map is sometimes also called a dictionary or a hash table.
But unlike a map, useState does not involve the use of (unique) keys . The following invalid code would have been more familiar to us as programmers.
1// INVALID REACT CODE AHEAD
2// Here "name" is the key and "John Doe" is the initial value
3const [name, setName] = useState("name", "John Doe")
4// Here "age" is the key and "35" is the initial value
5const [age, setAge] = useState("age", 35);
The React team decided not to go for this approach since there was a risk of duplicate keys. A unique key based approach would have made custom hooks unreliable.
As a beginner, I thought React was reading what is on the left side of the useState hook and doing some magic behind the scenes to make it work. In reality, useState is just a normal JavaScript function. All React knows, is how many times a component calls the useState hook and the order of these calls.
On the first render, React stores the data coming in via useState calls. You can think of this data store as a type of array, i.e. a list that is accessible via an index. Each stateful component gets it’s own list. Let us see how this works for our previous example:-
1const [name, setName] = useState("John Doe");
2const [age, setAge] = useState(35);
This results in a two element list with “John Doe” stored in the first element and “35” in the second. When the component re-renders, the data from this list is returned by the useState calls. React does this by simply reading the list from the beginning to the end. We have a problem if the read order does not match the order in which the list was originally created. This is why the useState hook is allowed only at the top level or a custom hook.
useState vs set
We know that the we can call the set
function from anywhere in the component. The set
function can modify any location of React’s state list. Why is the set
function “better” than useState in this regard? React knows how to match useState calls with its state list because of the “top-level only” restriction. This knowledge is transferred to the set
function. The set
function knows the index of the piece of state it is working with. The flexibility of the set
function originates from the rigidity of useState.
A Common Misconception
A common misconception about the useState hook is that the set
function would change the state of the component immediately. Let us take another look at our Counter
component.
1import { useState } from "react";
2
3const Counter = () => {
4 const [count, setCount] = useState(0);
5 const handleIncrementClick = () => {
6 setCount(count + 1);
7 console.log(count)
8 };
9
10 return (
11 <>
12 <p>cout is {count}</p>
13 <button onClick={handleIncrementClick}>increment</button>
14 </>
15 );
16};
17export default Counter;
Here we have added a console.log
below the setCount
call. Assuming that count
is 0, what will the console.log
statement print? Will it print 1?
At first glance, it might seem that it will. But remember that React has no idea what we do with the value (the two element array) returned by the useState hook. In the example above, we have used destructuring assignment to store it as two constants. We could have also stored it as two variables or just kept it as an array without the de-structuring assignment.
The setCount
function triggers a re-render and changes the value of count
stored by React.The updated value of count
will become available to the component only when we call useState on the next render.
Using the previous state to set new state
We have already seen that the set
function does not change the state of the component immediately. The component picks up the updated state value only during a re-render. This have consequences if the next state is based off current state, like in our Counter example.
1const handleIncrementClick = () => {
2 setCount(count + 1);
3 setCount(count + 1);
4};
Here we have added an extra setCount
call to handleIncrementClick
. If the count
starts at 0, what would be the value of count during the next render? The answer is 1, because the second setCount
call is redundant.
Assuming the starting value of count is 0, the previous code is equivalent to the following code:-
1const handleIncrementClick = () => {
2 setCount(0 + 1);
3 setCount(0 + 1);
4};
React has a solution for this issue. If we pass a function as argument to the set
function,it is treated as an updater function
and not as value that needed to be stored in state. The function is called with the current state as argument. The next state is set to the return value of the function. So, we must pass a function which accepts a single argument and returns the next value for state.
1const handleIncrementClick = () => {
2 setCount((c) => c + 1);
3 setCount((c) => c + 1);
4};
Now the handleIncrementClick
function will increment the count by 2. Admittedly, this is a contrived example, but you don’t normally want a lost update. So this is something you will have to watch out for.
Batching of state updates
Multiple set
calls in an event handler, raises some questions. Will the component re-render as soon as the first set
call is encountered? What becomes of the remaining set
calls? The answer is that, React batches state updates. React will render the component on screen only after all pending set
functions have finished executing.
Using objects as state
So far, we have looked only at how to use a primitive value i.e. number, as state. Using an object instead is similar but it could require more work.
React does not always trigger a re-render when we call the set
function. React compares the pending state with the current state. It re-renders the component only if the comparison shows a difference. This is an optimization to avoid unnecessary re-rendering. React uses JavaScript’s object.is
function for this purpose.
When we pass an object to a function, we are passing a reference to the object. There will be only one object in memory. This can pose a challenge, since we have to prove to React that the object we are passing to the set
function is different from the one already in state.
There are no issues if we want to replace an object with a brand new object. But it is more common to want to modify properties of an object. Mutating an object does not change it’s memory location. So instead of mutating objects, you will have to make a modified copy.
Don’t do this
1person.name = "Jane Doe"
2setPerson(person)
Do this instead
1setPerson({
2 ...person,
3 name: "Jane Doe"
4})
In some situations mutating an object might get the job done. But the React team strongly recommends treating objects in state as read-only.
What about deeply nested objects?
The treat objects as read-only rule, apply to nested objects as well. With deeply nested objects, making modified copies becomes tedious. If it gets too complicated, you might want to consider using the Immer library. Immer makes it very easy to work with immutable state.
What about arrays?
Arrays are objects in JavaScript and are subject to the same restrictions. JavaScript has many features that allows you to make copies and modified copies of arrays. The spread operator and functions like map, filter and slice return new arrays. If you want to perform a JavaScript array operation that does not return a new array, make a new array using the spread operator first, and perform the operation on the new array.
As with objects, you can use the immer library to treat arrays as mutable and still satisfy React’s immutability requirements.
Dealing with expensive initialization code
Remember that React uses the value passed to useState only during the initial render and simply ignores it during re-renders. This is inefficient if the initial value is the result of expensive computations.
1const [list, setList] = useState(createLargeArray());
React will use the result of createLargeArray
only once. But it is executed and its return value discarded on each render. React has a solution for this wasteful computation. You can put React in charge of calling the function.
1const [list, setList] = useState(createLargeArray);
We are now passing the function itself and not its return value. If we pass a function as argument to useState
, React will treat it as a special case. The function is considered as an initializer function, and not as a value that should be stored in state. React will call the function only on the initial render.