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 :
0
herecount
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.
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]:
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.
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`}
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></>);}
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).
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.
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:
array.push
, array.splice
, etc.) because non-primitive values are
reference values.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(", ")}<buttononClick={() => {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.
We can use everything as state value: primitive values (nullish values, booleans, number, string) and reference values (arrays, objects).
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.
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
// UsesetHandler(() => myNewHandler);// NOT (React will call myNewHandler with the previous value)setHandler(myNewHandler);
useState
with TypeScriptMost 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)
.
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
.
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.
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.
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.
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.
Exploitez des données serveur rapidement
Maîtrisez le développement d'applications complexes avec React
The Children Prop Pattern in React
Understanding React Hooks: JavaScript Fundamentals