ReactHustle

How to use React useState hook with Typescript

Jasser Mark Arioste

Jasser Mark Arioste

How to use React useState hook  with Typescript

Hello! Let us explore how to use React useState hook with typescript and avoid any compile time errors. We will try to explore different scenarios when initializing useState, what are the best practices, and bad practices as well, all complete in one full guide! Let's go!

Initializing useState using typescript type inference #

When we use typescript along with our react application, don't have to make any changes if the state is immediately initialized upon declaration. For example:


import React, { useState } from "react";

const ComponentA = () => {
  const [count, setCount] = useState(0);
  return <div>{count}</div>;
};

export default ComponentA;
123456789

Since we initialized useState as 0, the typescript engine in our vscode editor automatically infers that count is of type number. This is works for any type in our react application

count is infered as type number;

What is type inference? #

While typescript is a type language, it is not mandatory to specify the type for every variable. Type inference is when typescript infers/predicts the  type of certain variables based on its initialization when no explicit information is available. 


let x = 5 //x is a number
let greeting = "hello" //greeting is a string;

x = greeting // Compiler Error: Type 'string' is not assignable to type 'number'
12345

Typescript already infers that x is a number therefore, you cannot assign a string to it.

What's the problem when using type inference for useState? #

The problem with using type inference is when we initialize useState with incomplete data. This could be null, undefined or some object with incomplete data type.

Example when useState is initialized using null or undefined. It doesn't know the type of data we want for count and state which produces an error.

import React, { useEffect, useState } from "react";

const ComponentA = () => {
  const [count, setCount] = useState();
  const [state, setState] = useState(null);

  useEffect(() => {
    setCount(count + 1); //error: Object is possibly 'undefined'
    setState("hello"); //error: Argument of type '"hello"' is not assignable to parameter of type 'SetStateAction<null>'
  }, []);

  return <div>{count}</div>;
};

export default ComponentA;
123456789101112131415

Example when useState is initialized using incomplete data:


import React, { useEffect, useState } from 'react'

const ComponentB = () => {

  const [user, setUser] = useState({
    email: '',
  })

  useEffect(() => {
    setUser({
      email: "test@reacthustle.com",
      username: "foobar", //type error: Argument of type '{ email: string; username: string; }' is not assignable to parameter of type 'SetStateAction<{ email: string; }>'.
    });
  }, [])
  
  return (
    <div>ComponentB</div>
  )
}

export default ComponentB
12345678910111213141516171819202122

How do we fix the type errors above if we cannot rely on type inference?

Pretty simple actually. We have to explicitly set the type using generics feature of typescript.

How to explicitly set the useState type using generics? #

If you already have knowledge about generics, you should know that all hooks in react have a generic version that allows us to specify the types. It's like declaring what types of values can the setCount function accept or what types of values can the count variable give us. 

Explicitly setting types for simple values

First we have to add a generic parameter after useState. This is like saying: I want to declare count as a number or undefined and I want setCount to only accept number or undefined values. 


import React, { useEffect, useState } from "react";

const ComponentA = () => {
  const [count, setCount] = useState<number>();
  const [state, setState] = useState<string|null>(null);

  useEffect(() => {
    setCount(count + 1); //error: Object is possibly 'undefined'
    setState("hello"); //error: Argument of type '"hello"' is not assignable to parameter of type 'SetStateAction<null>'
  }, []);

  return <div>{count}</div>;
};

export default ComponentA;
12345678910111213141516

If we hover over count, we can see that it is explicitly set to number, undefined is still included since we didn't initialize it with a number value. As a result, setCount still has an error since we cannot add undefined to a number.

count has type number or undefined.

If we want to make sure that count is a number, we have to add guards to it before setting the value.

    ...
    if (typeof count === "number") {
      setCount(count + 1);
    }
    ...
12345

Explicitly setting types for complex objects

To set the types for objects, first we have to declare a type or an interface and use that as the generic parameter for the useState hook.

import React, { useEffect, useState } from "react";

type User = {
  email: string;
  username?: string;
};

// interface User {
//   email: string;
//   username?: string;
// };

const ComponentB = () => {
  const [user, setUser] = useState<User>({
    email: "",
  });

  useEffect(() => {
    //this works now
    setUser({
      email: "test@reacthustle.com",
      username: "foobar",
    });
  }, []);

  return <div>ComponentB</div>;
};

export default ComponentB;
1234567891011121314151617181920212223242526272829

How to initialize useState for array of objects. #

If we rely on type inference for arrays, the typescript compiler wouldn't know what types of array it will be and this will result in a type error. For example

import React, { useEffect, useState } from "react";

const ComponentB = () => {
  const [users, setUsers] = useState([]); //const users: never[]

  useEffect(() => {
    //error: Type '{ email: string; username: string; }' is not assignable to type 'never'
    setUsers([
      {
        email: "test@reacthustle.com",
        username: "foobar",
      },
    ]);
  }, []);

  return <div>ComponentB</div>;
};

export default ComponentB;
12345678910111213141516171819

To fix this, we have to once again use the generic parameter for useState to let typescript know that this is an array of users.

import React, { useEffect, useState } from "react";
//create a type that we're expecting
type User = {
  email: string;
  username?: string;
};

...
const [users, setUsers] = useState<User[]>([]); //const users: User[]
...

or
const [users, setUsers] = useState<Array<User>>([]); 
12345678910111213

When do you need use generics for useState? #

The best practice is to explicitly set the type using generic parameter if you don't know the type before initialization. For example, when fetching data from the backend, you can't rely on type inference if you don't have the values at compile time.


import React, { useEffect, useState } from "react";

//declare a type for the expected response from the backend.
type User = {
  email: string;
  username?: string;
};

const ComponentB = () => {
  const [users, setUsers] = useState<Array<User>>([]); //const users: User[]

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users").then(async (res) => {
      if (res.status == 200) {
        const _users = (await res.json()) as User[];
        setUsers(_users); // no error
      }
    });
  }, []);

  return <pre>{JSON.stringify(users, null, 4)}</pre>;
};

export default ComponentB;
12345678910111213141516171819202122232425

Is it possible to use <any> for useState type? #

While it definitely is possible to use <any>,  it would defeat the purpose of using typescript since we lose all the development experience it provides.

import React, { useEffect, useState } from "react";

const ComponentB = () => {
  const [users, setUsers] = useState<any[]>([]); //const users: any[]

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users").then(async (res) => {
      if (res.status == 200) {
        const _users = await res.json();
        setUsers(_users); // no error
      }
    });
  }, []);

  return (
    <div>
      {users.map((user, index) => {
        return <span key={index}>{user.emal}</span>; // There's no type error here. We would only see the bug when we run it.
      })}
    </div>
  );
};

export default ComponentB;
123456789101112131415161718192021222324

It won't be an awesome developer experience if we have to predict what the properties are for the user object. 

It just adds more headaches for us developers.

While there are definitely scenarios where using <any> makes sense (when you don't care about the object), it's best to avoid setting any as much as possible.

Why do you need to properly set the type for useState? #

It provides us with type safety which is the reason why we're using typescript in the first place. This helps us a lot during development since it provides us with autocomplete/intellisense, as well as compile-time type checking to easily avoid compile-time errors. 

Typescript providing intellisense for user type
Typescript providing type safety

Conclusion #

We learned on how to initialize useState with typescript using type inference and generics. We also learned to initialize different values such as primitive types (number, string,etc) , complex objects and arrays of objects. We also learned to avoid any as much as the type since it provide us with no benefits when using typescript.

Resources #

If you like tutorials like these, please leave a like or share this article! If you'd like to receive more tutorials directly to your inbox, subscribe to our newsletter!

Credits: Image by Ingo Jakubke from Pixabay

Share this post!

Related Posts