React Curriculum

Typing Hooks & Refs: State, Reducers & DOM Elements

Medium100% Free~25 mins#react#typescript#hooks#useref#usereducer#discriminated-unions
Interview Answer (30-Second Summary)

TypeScript can infer simple hook states, but union states require explicit generics like useState<User | null>(null). Type DOM refs with useRef<HTMLDivElement>(null) for read-only element access, while mutable container refs use explicit non-null initial values. Type useReducer with strict discriminated action unions.

Key Learning Objectives
✓

Type useState with union types, null initializers, and lazy initializer functions.

✓

Differentiate the two TypeScript overloads of useRef: DOM Ref (RefObject) vs Mutable Value Container (MutableRefObject).

✓

Construct strictly typed reducers using discriminated unions for actions and exhaustive switch checks.

✓

Infer custom hook return tuples using `as const` for strict array destructuring.

The Interview Problem & Context

Demonstrate how to type useState with nullable state, explain why useRef<HTMLInputElement>(null) creates a read-only ref, and build an exhaustively typed useReducer with discriminated unions.

1import React, { useState, useRef, useReducer } from 'react';
2
3interface User {
4 id: string;
5 name: string;
6 email: string;
7}
8
9// 1. Explicit generic for nullable union state
10export function UserProfile() {
11 const [user, setUser] = useState<User | null>(null);
12
13 // 2. DOM Ref vs Mutable Ref
14 // DOM Ref: Initial value null, type RefObject<HTMLInputElement> (current is read-only for consumer)
15 const inputRef = useRef<HTMLInputElement>(null);
16
17 // Mutable Container Ref: Initial value matches generic, type MutableRefObject<number> (current is writable)
18 const renderCountRef = useRef<number>(0);
19 renderCountRef.current += 1;
20
21 const focusInput = () => {
22 inputRef.current?.focus();
23 };
24
25 return <div>...</div>;
26}
27
28// 3. Exhaustively typed useReducer with Discriminated Unions
29interface State {
30 count: number;
31 step: number;
32}
33
34type Action =
35 | { type: 'INCREMENT' }
36 | { type: 'DECREMENT' }
37 | { type: 'SET_STEP'; payload: number }
38 | { type: 'RESET' };
39
40function counterReducer(state: State, action: Action): State {
41 switch (action.type) {
42 case 'INCREMENT':
43 return { ...state, count: state.count + state.step };
44 case 'DECREMENT':
45 return { ...state, count: state.count - state.step };
46 case 'SET_STEP':
47 return { ...state, step: action.payload };
48 case 'RESET':
49 return { count: 0, step: 1 };
50 default: {
51 // Exhaustiveness check: If a new Action type is added, TS flags a compile error here!
52 const _exhaustiveCheck: never = action;
53 return state;
54 }
55 }
56}
Deep Architectural Explanation

Typing Hooks & Refs

1. useState Generics:

If initial state is empty or null, TypeScript infers null unless an explicit generic is provided:

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

2. The Two Overloads of useRef:

TypeScript defines two distinct overloads for useRef:

  1. DOM Ref (RefObject<T>):
    useRef<HTMLInputElement>(null)
    • current is typed as HTMLInputElement | null.
    • current is read-only because React attaches and manages the DOM node during the commit phase!
  2. Mutable Value Container (MutableRefObject<T>):
    useRef<number>(0)
    • current is typed as number and is mutable. You can write ref.current = 5.

3. Discriminated Unions for Reducers:

Each action object has a common discriminant property type. Inside each case, TypeScript automatically narrows the type of action.payload.
Adding const _exhaustive: never = action in default ensures compile-time errors if a team member adds an action type without handling it!

Common Interview Traps & Pitfalls
✕

Calling `useState(null)` without `<Type | null>`, locking state to type `null` forever.

✕

Typing DOM refs without `null` as initial value (`useRef<HTMLInputElement>()`), which selects the wrong mutable overload.

✕

Typing reducer actions as `{ type: string; payload?: any }` instead of strict discriminated unions.

Senior Interviewer Follow-Up Probes
?

How does TypeScript infer types for `useContext` when context is initialized with `null`?

?

How do you write a custom hook that returns a type-safe tuple vs an object?

?

What is the difference between `RefObject<T>` and `ForwardedRef<T>`?

Frequently Asked Interview Questions
Why do we write useState<User | null>(null)?

If you write useState(null) without a generic, TypeScript infers the state type as null. It will reject any subsequent call like setUser({ name: "Alice" }). Providing <User | null> allows state to transition from null to a User object.

TypeScript treats useRef<HTMLInputElement>(null) as a DOM RefObject. In React, DOM element references are assigned and managed by React during commit, so TypeScript marks current as readonly to prevent accidental consumer mutation.

Type it as useRef<string | null>(null) or use the union type where null is included in the generic parameter without selecting the DOM overload.

Assigning the action to a variable of type never in the default case (const _check: never = action). If an unhandled action type exists in the union, TypeScript throws a compile-time type mismatch error.

Without "as const", TypeScript infers an array union type like (ValueType | SetterType)[]. Using "as const" asserts a fixed tuple [ValueType, SetterType], enabling accurate type inference when destructuring.

Provide an explicit generic: useMemo<CalculatedData>(() => computeExpensiveValue(data), [data]).