Skip to main content

Hooks

Hooks are supported in @types/react from v16.8 up.

useState

Type inference works very well for simple values:

const [state, setState] = useState(false);
// `state` is inferred to be a boolean
// `setState` only takes booleans

If you need to use a complex type that you've relied on inference for, you can use typeof to capture the inferred type.

However, many hooks are initialized with null-ish default values, and you may wonder how to provide types. Explicitly declare the type, and use a union type:

const [user, setUser] = useState<User | null>(null);

// later...
setUser(newUser);

You can also use type assertions if a state is initialized soon after setup and always has a value after:

const [user, setUser] = useState<User>({} as User);

// later...
setUser(newUser);

This temporarily "lies" to the TypeScript compiler that {} is of type User. You should follow up by setting the user state — if you don't, the rest of your code may rely on the fact that user is of type User and that may lead to runtime errors.

useCallback

You can type the useCallback just like any other function.

const memoizedCallback = useCallback(
(param1: string, param2: number) => {
console.log(param1, param2)
return { ok: true }
},
[...],
);
/**
* VSCode will show the following type:
* const memoizedCallback:
* (param1: string, param2: number) => { ok: boolean }
*/

Note that for React < 18, the function signature of useCallback typed arguments as any[] by default:

function useCallback<T extends (...args: any[]) => any>(
callback: T,
deps: DependencyList,
): T;

In React >= 18, the function signature of useCallback changed to the following:

function useCallback<T extends Function>(callback: T, deps: DependencyList): T;

Therefore, the following code will yield "Parameter 'e' implicitly has an 'any' type." error in React >= 18, but not <17.

// @ts-expect-error Parameter 'e' implicitly has 'any' type.
useCallback((e) => {}, []);
// Explicit 'any' type.
useCallback((e: any) => {}, []);

useReducer

You can use Discriminated Unions for reducer actions. Don't forget to define the return type of reducer, otherwise TypeScript will infer it.

import { useReducer } from "react";

const initialState = { count: 0 };

type ACTIONTYPE =
| { type: "increment"; payload: number }
| { type: "decrement"; payload: string };

function reducer(
state: typeof initialState,
action: ACTIONTYPE,
): typeof initialState {
switch (action.type) {
case "increment":
return { count: state.count + action.payload };
case "decrement":
return { count: state.count - Number(action.payload) };
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement", payload: "5" })}>
-
</button>
<button onClick={() => dispatch({ type: "increment", payload: 5 })}>
+
</button>
</>
);
}

View in the TypeScript Playground

Usage with Reducer from redux

In case you use the redux library to write reducer function, It provides a convenient helper of the format Reducer<State, Action> which takes care of the return type for you.

So the above reducer example becomes:

import { Reducer } from 'redux';

export function reducer: Reducer<AppState, Action>() {}
Providing explicit types for useReducer

In most cases, type inference for useReducer should work reliably. When inference fails, the state and action types can be explicitly provided using the following syntax, where the action type is wrapped in a single-element tuple.

const [state, dispatch] = useReducer<typeof initialState, [ACTIONTYPE]>(
reducer,
initialState,
);

useEffect / useLayoutEffect

Both of useEffect and useLayoutEffect are used for performing side effects and return an optional cleanup function which means if they don't deal with returning values, no types are necessary. When using useEffect, take care not to return anything other than a function or undefined, otherwise both TypeScript and React will yell at you. This can be subtle when using arrow functions:

function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;

useEffect(
() =>
setTimeout(() => {
/* do stuff */
}, timerMs),
[timerMs],
);
// bad example! setTimeout implicitly returns a number
// because the arrow function body isn't wrapped in curly braces
return null;
}
Solution to the above example
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;

useEffect(() => {
setTimeout(() => {
/* do stuff */
}, timerMs);
}, [timerMs]);
// better; use the void keyword to make sure you return undefined
return null;
}

useRef

useRef always returns a RefObject<T> in current @types/react. An initial value is required, and the returned .current is typed based on it. (MutableRefObject is deprecated and only kept for backwards compatibility.)

Option 1: DOM element ref

To access a DOM element: provide the element type as a generic and pass null as the initial value. React manages .current for you, and TypeScript expects you to pass this ref to an element's ref prop:

function Foo() {
// - If possible, prefer as specific as possible. For example, HTMLDivElement
// is better than HTMLElement and way better than Element.
// - Technical-wise, this returns RefObject<HTMLDivElement>
const divRef = useRef<HTMLDivElement>(null);

useEffect(() => {
// Note that ref.current may be null. This is expected, because you may
// conditionally render the ref-ed element, or you may forget to assign it
if (!divRef.current) throw Error("divRef is not assigned");

// Now divRef.current is sure to be HTMLDivElement
doSomethingWith(divRef.current);
});

// Give the ref to an element so React can manage it for you
return <div ref={divRef}>etc</div>;
}

If you are sure that divRef.current will never be null, it is also possible to use the non-null assertion operator !:

const divRef = useRef<HTMLDivElement>(null!);
// Later... No need to check if it is null
doSomethingWith(divRef.current);

Note that you are opting out of type safety here - you will have a runtime error if you forget to assign the ref to an element in the render, or if the ref-ed element is conditionally rendered.

Tip: Choosing which HTMLElement to use

Refs demand specificity - it is not enough to just specify any old HTMLElement. If you don't know the name of the element type you need, you can check lib.dom.ts or make an intentional type error and let the language service tell you:

image

Option 2: Mutable value ref

To hold a mutable value across renders without re-rendering on change: pass the initial value you want — React doesn't manage .current for you here, you write to it manually.

function Foo() {
const intervalRef = useRef<number | null>(null);

useEffect(() => {
intervalRef.current = window.setInterval(() => {
/* ... */
}, 1000);
return () => {
if (intervalRef.current !== null) clearInterval(intervalRef.current);
};
}, []);

return (
<button
onClick={() => {
/* clearInterval the ref */
}}
>
Cancel timer
</button>
);
}

See also

useImperativeHandle

In React 19, ref is a regular prop on function components, so useImperativeHandle is called with the ref prop directly — no forwardRef needed.

// Countdown.tsx
import { useImperativeHandle, Ref } from "react";

export type CountdownHandle = {
start: () => void;
};

type CountdownProps = {
ref?: Ref<CountdownHandle>;
};

const Countdown = ({ ref }: CountdownProps) => {
useImperativeHandle(ref, () => ({
// start() has type inference here
start() {
alert("Start");
},
}));

return <div>Countdown</div>;
};
// The component using the Countdown component
import { useEffect, useRef } from "react";
import Countdown, { CountdownHandle } from "./Countdown.tsx";

function App() {
const countdownEl = useRef<CountdownHandle>(null);

useEffect(() => {
if (countdownEl.current) {
// start() has type inference here as well
countdownEl.current.start();
}
}, []);

return <Countdown ref={countdownEl} />;
}

If you still maintain code that targets React < 19, see the forwardRef section for the legacy approach using forwardRef<CountdownHandle, CountdownProps>.

Custom Hooks

If you are returning an array in your Custom Hook, you will want to avoid type inference as TypeScript will infer a union type (when you actually want different types in each position of the array). Instead, use TS 3.4 const assertions:

import { useState } from "react";

export function useLoading() {
const [isLoading, setState] = useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as const; // infers [boolean, typeof load] instead of (boolean | typeof load)[]
}

View in the TypeScript Playground

This way, when you destructure you actually get the right types based on destructure position.

Alternative: Asserting a tuple return type

If you are having trouble with const assertions, you can also assert or define the function return types:

import { useState } from "react";

export function useLoading() {
const [isLoading, setState] = useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as [
boolean,
(aPromise: Promise<any>) => Promise<any>,
];
}

A helper function that automatically types tuples can also be helpful if you write a lot of custom hooks:

function tuplify<T extends any[]>(...elements: T) {
return elements;
}

function useArray() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return [numberValue, functionValue]; // type is (number | (() => void))[]
}

function useTuple() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return tuplify(numberValue, functionValue); // type is [number, () => void]
}

Note that the React team recommends that custom hooks that return more than two values should use proper objects instead of tuples, however.

More Hooks + TypeScript reading:

If you are writing a React Hooks library, don't forget that you should also expose your types for users to use.

Example React Hooks + TypeScript Libraries: