C.W.K.
Stream
Lesson 02 of 07 · published

React 19 Hooks: useState, useReducer, Custom Hooks

~10 min · frameworks, react, hooks

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Hooks are typed by React itself. Your job is to give them the right initial values."

useState typing

useState infers the state type from its initial value. useState(0) is [number, Dispatch<SetStateAction<number>>]. useState('hi') is [string, ...]. When the initial value is too narrow (null, undefined) or you want a union, pass an explicit generic: useState<User | null>(null).

useReducer typing

useReducer's typing comes from the reducer signature. function reducer(state: State, action: Action): State { ... }. useReducer(reducer, initialState) infers both state and action types from the reducer. Discriminated unions on Action give you exhaustive case checking inside the reducer.

Custom hooks

A custom hook is a function whose name starts with use and which calls other hooks. Type it like any other function. function useUser(id: number): { user: User | null; loading: boolean }. Return tuples for state+setter pairs: function useCounter(initial: number): [number, () => void].

React's types are extensive and accurate. If your hooks are losing types, you've probably passed any somewhere. Trace back to find where, fix that, and the types flow correctly through the rest.

Code

Hooks — useState, useReducer, custom·tsx
import { useState, useReducer, useEffect } from 'react';

// useState — inference handles the common case.
function Counter() {
  const [count, setCount] = useState(0);          // number
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

// useState — explicit generic for nullable/union initial.
function UserProfile() {
  const [user, setUser] = useState<User | null>(null);
  // ...
  return null;
}

// useReducer — discriminated union for actions.
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'set'; value: number };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'increment': return state + 1;
    case 'decrement': return state - 1;
    case 'set': return action.value;
  }
}

function CounterAdvanced() {
  const [count, dispatch] = useReducer(reducer, 0);
  return (
    <button onClick={() => dispatch({ type: 'increment' })}>{count}</button>
  );
}

// Custom hook.
function useDelayed<T>(value: T, ms: number): T {
  const [delayed, setDelayed] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDelayed(value), ms);
    return () => clearTimeout(id);
  }, [value, ms]);
  return delayed;
}

External links

Exercise

Write a custom useToggle(initial: boolean): [boolean, () => void] hook. Then write a component that uses it for a 'show/hide details' toggle. Confirm the destructuring gives you the right types.
Hint
Inside, useState<boolean>(initial) and return [value, () => setValue(v => !v)]. The functional setter pattern handles the toggle without stale-state bugs.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.