Typing React Hooks, Generics, and Refs0%

Typing React Hooks, Generics, and Refs

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Typing React Hooks, Generics, and Refs

While TypeScript's type inference handles simple hooks like useState(0) automatically, enterprise applications frequently require explicit generic typing for union states, null initializations, DOM references, and generic reusable custom hooks.


1. Typing useState

Inferred Primitives

TypeScript
const [count, setCount] = useState(0); // Inferred as number
const [text, setText] = useState(""); // Inferred as string

Explicit Generics for Complex Types and Nullable State

When a state object starts as null before network data loads, type inference as null will error when assigning an object later. Provide an explicit generic union:

TSX
interface UserProfile {
id: string;
name: string;
email: string;
}
 
// State can be UserProfile or null
const [user, setUser] = useState<UserProfile | null>(null);
 
// Type unions for status flags
type AuthStatus = "idle" | "authenticating" | "authenticated" | "error";
const [status, setStatus] = useState<AuthStatus>("idle");

2. Typing useRef

useRef has two distinct use cases in TypeScript with very different type signatures:

Case A: DOM Element Reference (Read-Only Ref)

When referencing a physical DOM element, pass the HTML element type and initialize with null:

TSX
import React, { useRef, useEffect } from "react";
 
export function AutoFocusSearchInput() {
// Read-only ref bound to an HTMLInputElement
const inputRef = useRef<HTMLInputElement>(null);
 
useEffect(() => {
// Optional chaining required because inputRef.current might be null before mount
inputRef.current?.focus();
}, []);
 
return (
<input
ref={inputRef}
type="text"
placeholder="Type here..."
className="px-4 py-2 bg-slate-800 text-white rounded border border-slate-700"
/>
);
}

Case B: Mutable Mutable Variable (Value Container)

When using useRef as an instance variable that persists across renders without triggering re-renders (like a timer ID or WebSocket handle), provide the type without null initialization if not needed:

TSX
// Mutable ref container holding a timer ID
const timerRef = useRef<number | undefined>(undefined);
 
const startTimer = () => {
timerRef.current = window.setInterval(() => {
console.log("Tick");
}, 1000);
};
 
const stopTimer = () => {
if (timerRef.current !== undefined) {
clearInterval(timerRef.current);
}
};

3. Typing useReducer

Discriminated unions provide complete type safety for reducer actions:

TSX
interface State {
count: number;
error: string | null;
}
 
type Action =
| { type: "INCREMENT"; payload: number }
| { type: "DECREMENT"; payload: number }
| { type: "RESET" }
| { type: "SET_ERROR"; payload: string };
 
function counterReducer(state: State, action: Action): State {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + action.payload, error: null };
case "DECREMENT":
return { ...state, count: state.count - action.payload, error: null };
case "RESET":
return { count: 0, error: null };
case "SET_ERROR":
return { ...state, error: action.payload };
default: {
const _exhaustive: never = action;
return state;
}
}
}

4. Generic Custom Hooks

Generic custom hooks allow components to consume typed utilities dynamically:

TSX
import { useState, useEffect } from "react";
 
interface FetchState<T> {
data: T | null;
isLoading: boolean;
error: Error | null;
}
 
export function useFetchData<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
isLoading: true,
error: null,
});
 
useEffect(() => {
let isMounted = true;
 
fetch(url)
.then((res) => res.json())
.then((data: T) => {
if (isMounted) setState({ data, isLoading: false, error: null });
})
.catch((error: Error) => {
if (isMounted) setState({ data: null, isLoading: false, error });
});
 
return () => {
isMounted = false;
};
}, [url]);
 
return state;
}
 
// Consumption with full type inference:
interface Order {
id: string;
total: number;
}
 
// data is strictly typed as Order[] | null
// const { data, isLoading } = useFetchData<Order[]>("/api/orders");

Interactive Quiz

Practice Quiz

Question 1
What happens if you initialize useState with null without a generic type parameter: const [user, setUser] = useState(null)?
Question 2
How should you type a ref intended to attach to an HTML <canvas> element?
Question 3
In a typed reducer, what is the benefit of the const _exhaustive: never = action statement in the default case?
Question 4
Why must inputRef.current be checked with optional chaining (inputRef.current?.focus()) before calling methods?
Question 5
How do generics benefit custom hooks like useFetchData<T>(url)?
Next Lesson

Typing Synthetic Events and Form Elements

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext