DEV Community

Cover image for Using React useState hook with TypeScript
Milosz Piechocki
Milosz Piechocki

Posted on • Edited on • Originally published at codewithstyle.info

Using React useState hook with TypeScript

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 deeper understanding of hooks' types might prove very useful. In this article, 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).

If you're only interested in practical examples, skip to the next section.

Overloads

function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
Enter fullscreen mode Exit fullscreen mode

As you can see, there are two versions of 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 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 parameter. It's type is a union of S and () => S. Why would you pass a function that returns initial state instead of passing the initial state directly? Computing initial state can be expensive. It's only needed during 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 - 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. 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 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 initial state to useState.

As we saw in the type definition, the type argument S for the parameterless defaults to 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>. It's really bad, as nothing would stop us from calling it with 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>
    );
}
Enter fullscreen mode Exit fullscreen mode

In order to fix this issue, we need to pass a type argument to setState. We treat pill as text in JSX, so our fist 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'>();
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

...unless we check the value first:

// ✅ No errors!
{pill && <span>You chose {pill.toUpperCase()} pill!</span>}
Enter fullscreen mode Exit fullscreen mode

Clearable state

Another kind of situation when you would provide a type argument to useState is when 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>);
}
Enter fullscreen mode Exit fullscreen mode

Since 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');
Enter fullscreen mode Exit fullscreen mode

Summary

Wrapping up, we've analysed the type definitions of useState function thoroughly. Based on this information, we saw when providing the type argument to useState might be necessary and when the inferred type is sufficient.

I like how hooks are great example of how much information can be read from type definitions. They really show off the power of static typing!

Want to learn more?

Did you like this TypeScript article? I bet you'll also like my book!

⭐️ Advanced TypeScript ⭐️

Top comments (0)