How to use React useState hook with Typescript
Jasser Mark Arioste
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
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.
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.
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 #
React Typescript Cheatsheet - https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks
Typescript Generics - https://basarat.gitbook.io/typescript/type-system/generics
Type Inference - https://basarat.gitbook.io/typescript/type-system/type-inference
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