SyntaxStudy
Sign Up
TypeScript Typing Hooks: useState, useRef, useReducer
TypeScript Beginner 1 min read

Typing Hooks: useState, useRef, useReducer

React hooks are generics that TypeScript can infer or be explicitly told what types they work with. useState infers the state type from the initial value, but you should provide the type parameter explicitly when the initial value does not fully represent the type — for example, when the initial value is null but the state will later hold a full object. useRef has two overloads with importantly different semantics. When you pass null as the initial value and provide the type parameter, you get a RefObject where current is read-only — this is for DOM element refs. When you provide an initial non-null value, you get a MutableRefObject where current is writable — this is for storing mutable values that should not trigger re-renders. Mixing these up is a common source of TypeScript errors. useReducer is the hook that benefits most from TypeScript because the reducer function is a natural discriminated union handler. You define a State type and an Action type (typically a discriminated union), and the reducer function's type is (state: State, action: Action) => State. TypeScript ensures that every action type is handled and that each case only accesses the properties relevant to that action.
Example
import { useReducer, useRef, useState, type RefObject } from "react";

// useState — explicit generic when initial value is null
interface UserProfile { id: number; name: string; avatar: string }

function useProfile() {
    const [profile, setProfile] = useState<UserProfile | null>(null);
    const [loading, setLoading] = useState(false); // inferred: boolean

    async function load(userId: number) {
        setLoading(true);
        const res  = await fetch(`/api/users/${userId}`);
        const data = (await res.json()) as UserProfile;
        setProfile(data);
        setLoading(false);
    }

    return { profile, loading, load };
}

// useRef — DOM ref (RefObject, current is readonly)
function FocusInput() {
    const inputRef: RefObject<HTMLInputElement> = useRef(null);

    const focusInput = () => {
        inputRef.current?.focus(); // safe optional chain
    };

    return (
        <>
            <input ref={inputRef} type="text" />
            <button onClick={focusInput}>Focus</button>
        </>
    );
}

// useReducer — typed state machine
interface CartState { items: { id: number; qty: number }[]; total: number }

type CartAction =
    | { type: "ADD";    id: number; price: number }
    | { type: "REMOVE"; id: number; price: number }
    | { type: "CLEAR" };

function cartReducer(state: CartState, action: CartAction): CartState {
    switch (action.type) {
        case "ADD":
            return { ...state, items: [...state.items, { id: action.id, qty: 1 }], total: state.total + action.price };
        case "REMOVE":
            return { ...state, items: state.items.filter(i => i.id !== action.id), total: state.total - action.price };
        case "CLEAR":
            return { items: [], total: 0 };
    }
}

function Cart() {
    const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
    return <div>Items: {cart.items.length}, Total: ${cart.total}</div>;
}