React and TypeScript: `useState` Hook
This lesson is a detailed analysis of typing the `useState` hook in TypeScript.
We'll cover the following
Overview
React hooks are a recent addition to React that make function components have almost the same capabilities as class components. Most of the time, using React hooks in TypeScript is straightforward.
However, there are some situations when a deeper understanding of a hook’s type might prove useful. In this lesson, we’re going to focus on the useState
hook.
I’m going to assume that you have a basic understanding of this hook. If this is not the case, please read this first.
Reading the types
First of all, let’s take a look at the type signature of useState
. You’ll see how much information you can extract solely from types, without looking at the docs or the implementation.
Overloads
function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
As you can see, there are two versions of the useState
function. TypeScript lets you define multiple type signatures for a function as it is often the case in JavaScript that a function supports different types of parameters. Multiple type signatures for a single function are called overloads.
Both overloads are generic functions. The type parameter S
represents the type of the piece of state stored by the hook. The type argument in the second overload can be inferred from initialState
. However, in the first overload, it defaults to undefined
unless the type argument is explicitly provided. If you don’t pass the initial state to useState
, you should provide the type argument explicitly.
useState
parameters
The first overload doesn’t take any parameters; it’s used when you call useState
without providing any initial state.
The second overload accepts initialState
as a parameter. Its type is a union of S
and () => S
. Why would you pass a function that returns the initial state instead of passing the initial state directly? Computing the initial state can be expensive. It’s only needed when the component is mounted. However, in a function component it would be calculated on every render. Therefore, you have an option to pass a function that calculates initial state, meaning expensive computation will only be executed once, not on every render.
useState
return type
Let’s move to the return type. It’s a tuple in both cases. A tuple is like an array that has a specific length and contains elements with specific types.
For the second overload, the return type is [S, Dispatch<SetStateAction<S>>]
. The first element of the tuple has type S
, the type of the piece of state. It will contain the value retrieved from the component’s state.
The second element’s type is Dispatch<SetStateAction<S>>
. Dispatch<A>
is simply defined as (value: A) => void
, a function that takes a value and doesn’t return anything. SetStateAction<S>
is defined as S | ((prevState: S) => S)
. Therefore, the type of Dispatch<SetStateAction<S>>
is actually (value: S | ((prevState: S) => S)) => void
. It is a function that takes either an updated version of the piece of state OR a function that produces the updated version based on the previous version. In both cases, we can deduce that the second element of the tuple returned by setState
is a function that we can call to update the component’s state.
The return type of the first overload is the same, but here instead of S
, S | undefined
is used anywhere. If we don’t provide the initial state it will store undefined
initially. It means that undefined
has to be included in the type of the piece of state stored by the hook.
Usage examples
Most of the time you don’t need to bother with providing type arguments to useState
, the compiler will infer the correct type for you. However, in some situations, type inference might not be enough.
Empty initial state
The first type of situation is when you don’t want to provide the initial state to useState
.
As we saw in the type definition, the type argument S
for the parameterless defaults as undefined
. Therefore, the type of pill
should be inferred to undefined
. However, due to a design limitation in TypeScript, it’s actually inferred to any
.
Similarly, setPill
's type is inferred to React.Dispatch<any>
. This is really bad, as nothing would stop us from calling it with an invalid argument: setPill({ hello: 5 })
.
export const PillSelector: React.FunctionComponent = () => {
const [pill, setPill] = useState();
return (
<div>
<button onClick={() => setPill('red')}>Red pill</button>
<button onClick={() => setPill('blue')}>Blue pill</button>
<span>You chose {pill} pill!</span>
</div>
);
}
In order to fix this issue, we need to pass a type argument to setState
. We treat pill
as text in JSX, so our first bet could be string
. However, let’s be more precise and limit the type to only allow values that we expect.
const [pill, setPill] = useState<'red' | 'blue'>();
Note that the inferred type of pill
is now "red" | "blue" | undefined
because this piece of state is initially empty. With strictNullChecks
enabled, TypeScript wouldn’t let us call anything on pill
:
// 🛑 Object is possibly 'undefined'.ts(2532)
<span>You chose {pill.toUpperCase()} pill!</span>
…unless we check the value first:
// ✅ No errors!
{pill && <span>You chose {pill.toUpperCase()} pill!</span>}
Clearable state
Another situation where you would provide a type argument to useState
is when the initial state is defined, but you want to be able to clear the state later.
export const PillSelector: React.FunctionComponent = () => {
const [pill, setPill] = useState('blue');
return (<div>
<button onClick={() => setPill('red')}>Red pill</button>
<button onClick={() => setPill('blue')}>Blue pill</button>
// 🛑 Argument of type 'undefined' is not assignable
// to parameter of type 'SetStateAction<string>'.
<button onClick={() => setPill(undefined)}>Reset</button>
{pill && <span>You chose {pill.toUpperCase()} pill!</span>}
</div>);
}
Since the initial state is passed to useState
, the type of pill
gets inferred to string
. Therefore, when you try to pass undefined
to it, TypeScript will error.
You can fix the problem by providing the type argument.
const [pill, setPill] = useState<'blue' | 'red' | undefined>('blue');
Get hands-on with 1400+ tech skills courses.