All you need to know about React.useState

A complete guide to React.useState hook mechanisms.

8 min read - 1498 words

useState is a hook provided by React to provide state to functional components. It is one of the most used hooks of React. Its interface is just one line:

typescript
const [count, setCount] = React.useState(0);

The interface consists of :

  • an initializer value: 0 here
  • an array as return value with:
    • the current state value at the first index: here unpacked to count
    • a setter at the second index: here setCount

The initializer value will be used on the first render to initialize the state with this value. For re-renders, React will not use the initializer.

The first item of the return value (here count) will hold the current state value.

The second item is a function called the setter. Called with a new value, it will mutate the state value and trigger a re-render if needed.

Convention

When destructuring the useState return value, always name the setter with the prefix set followed by the variable name in Capital Camel Case (MyVariable).

Example: setter for usersCount will be named setUsersCount.

Today, we will go a bit further to review everything to know about the useState hook (items followed by ⚛️ have a specific paragraph in the [official React documentation][1]:

  • lazy initial state: using a function as initializer value ⚛️
  • functional updates: use a function for updating ⚛️
  • bailing out state updates: setter triggers a re-render only on strict value change ⚛️
  • state values (primitive value, array, object, function)
  • typing with TypeScript
  • state updates are asynchronous
  • stability of the setter and implications
  • batched updates (before & after React v18) ⚛️
  • state updates and transitions
Do I need to know all this?

Well, if you are already familiar with the useState hook, you are fine. However, knowing about all these points will help you understand subtleties and make you more proficient in debugging codes and performance issues. In short, it will make you a better React developer.

Also, when I interview for potential hires, I looooveee 😍 to ask this kind of question: "What can you tell me about the useState hook in React?". Depending on the answer, it will give me a reasonable estimate of how much the candidate knows about React.

Basics ⚽

Lazy initial state ⚛️

In some cases, the initial state of the useState hook may take time to compute. In this case, you can use the lazy initial state value version by using a function that will return the initial state. This function will be called on the first render and only on the first render to initialize the state.

For instance, consider the following piece of code:

typescript
/**
* Create a grid of 1000 rows by 1000 columns
*/
function createInitialGridValues(initialGridValue = 1) {
const rows = new Array(1000).fill(0);
return rows.map((_, rowIndex) =>
new Array(1000)
.fill(initialGridValue)
.map((_, columnIndex) => columnIndex * rowIndex)
);
}
export function Grid({ initialGridValue }: { initialGridValue: number }) {
const initialGridValues = createInitialGridValues(initialGridValue);
const [gridValues, setGridValues] = React.useState(initialGridValues);
// do something with `gridValues`
}

createInitialGridValues may be compute time intensive (even more if you use a 10k by 10k grid). Therefore we prefer to implement it like this:

typescript
export function Grid({ initialGridValue }: { initialGridValue: number }) {
const [gridValues, setGridValues] = React.useState(() =>
createInitialGridValues(initialGridValue)
);
// do something with `gridValues`
}

Functional updates ⚛️

It's a valuable feature to mutate the state if the new state value depends on the previous. The setter can accept a function that receives the last state value and return the new state value.

typescript
export function MyComponent() {
const [count, setCount] = React.useState(0);
return (
<>
Count: {count}
<button onClick={() => setCount((prevCount) => prevCount - 1)}>-</button>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
</>
);
}
What is it preferred to `setCount(count + 1)`?

If the user rapidly clicks multiple times on the button, React may not have the time to re-render your component: on the second click, the count value would be the "old" value leading to inconsistent behavior and React would bail out the state updates (see below).

Bailing out state updates ⚛️

React is smart: if we call the setter with a new value that equals the old value (shallow comparison), React will not change the state. No re-render will be triggered. Effects will not be fired. It avoids unnecessary re-renders.

Important

React use Object.is for comparison between old and new state values: it's similar to the strict equality operator === with some differences.

This last bit is essential to know as it's a mistake that I found from time to time in the code that I reviewed. I teach to use functional programming to mutate the state to avoid these bugs:

  • Prefer destructuring and spread operators for arrays and objects ;
  • Never use in-place methods or mutating expressions (like array.push, array.splice, etc.) because non-primitive values are reference values.
Example

Consider the following code:

typescript
type MyItem = { title: string };
export function MyComponent() {
const [items, setItems] = React.useState<MyItem[]>([]);
return (
<>
{items.map((x) => x.title).join(", ")}
<button
onClick={() => {
items.push({ title: `Title ${items.length}` });
setItems(items);
}}
>
Add item
</button>
</>
);
}

If you click the Add item button, no re-render will be triggered: the items.push method mutates the items array in place.

The reference to the value stays the same while the value has changed in memory. It's better to use functional expressions like:

typescript
setItems(items => {
const newItem = { title: `Title ${items.length}` };
setItems(items => [...items, newItem]);
}

The same goes for object values and all non-primitive values.

State values

We can use everything as state value: primitive values (nullish values, booleans, number, string) and reference values (arrays, objects).

Functional expressions

I repeat it but it's crucial: use functional expressions (destructuring, spread operator, etc.) to update the state. If you don't create a new reference, React will not trigger a re-render.

Function as state value

In some exotic use cases, you may want to store functions as state values. In this case, do not forget to set the new value using the functional update version. If you don't, React will call your function will the previous state value:

typescript
// Use
setHandler(() => myNewHandler);
// NOT (React will call myNewHandler with the previous value)
setHandler(myNewHandler);

Intermediate 🏅

Typing useState with TypeScript

Most of the time, type inference will do the job: you will not need to type the state explicitly.

Sometimes, you may need to use generics to type the state explicitly, for instance, when using null values: useState<UserDetails | null>(null).

Setter is asynchronous

The state value is set asynchronously in React when using the setter, which means that the state is only changed when the component is re-rendered by React. It's a mechanism that allows the earlier stated bail out of updates.

In other words, when you call the setter, the code is not executed synchronously. To React to state changes, you must use React.useEffect.

Setter stability

The setter (second element returned by React.useState) is stable between re-renders: the function setState (setCount in our first example) is a reference to the same function during all re-renders. It's safe to omit it from the hooks dependency list (useEffect, useCallback, useMemo...).

It's why react-hooks/exhaustive-deps eslint rule lets you omit the setter from deps list.

Advanced 🏆

Batched updates ⚛️

React useState official doc and the React v18 upgrade guide explain this feature.

It's a performance feature: React groups multiple state updates into a single re-render for better performance.

Before v18, React batches state updates from React event handlers only (for instance, multiple state updates triggered from the same onClick handler).

From v18 and using createRoot, React batches all updates. It includes state updates from timeouts, promises, and native event handlers.

Note

React keeps things consistent: it only batches state updates from different user-initiated events.

On the contrary, state updates from the same user-event (for instance, when clicking a button twice) are treated separately.

You may want to consider this if a state change triggers a network call or some compute-intensive code.

React v18 Transitions: urgent vs non-urgent state updates

Starting from v18, React introduces the concept of Transitions (see React v18.0 – React Blog): using the startTransition function or the React.useTransition hook, you may declare some state updates as non-urgent vs. urgent updates like user-triggered state updates (click on a button, text input, etc.).

You can check this article Don't Stop Me Now: How to Use React useTransition() hook for an example about it.

References

Formations

React Query1 jour

React Query

Exploitez des données serveur rapidement

En savoir plus

Articles

Go to article

The Children Prop Pattern in React

While it is one of the most underused patterns in React, the Children Prop Pattern is a powerful tool for building reusable components. But it has another aspect that makes it even more interesting to understand and use: re-renders optimization.
ReactPatternsPerformance
November 24, 20224 min read3 réactions
Go to article

Understanding React Hooks: JavaScript Fundamentals

Hooks are hugely related to more general JavaScript concepts like closures, memoization, and primitive vs. reference values. Correctly understanding these concepts and their role in React Hooks is essential to use them properly.
ReactHooksFundamentals
September 26, 202211 min read4 réactions