Back to React tutorials
Intermediate18 min read

Hooks In Depth

Master useState, useEffect, useContext, useReducer, useMemo, useCallback, and custom hooks for production React.

useState and useReducer

useState handles simple independent values. useReducer manages state through `(state, action) => newState` reducers — ideal for forms with many fields, wizard steps, and state machines with explicit transitions.

Dispatch actions with typed action unions in TypeScript. Colocate reducer with component or extract to module for testing. useReducer pairs well with Context for lightweight global state without external libraries.

  • useState for simple local state
  • useReducer for complex transitions
  • Typed action unions in TypeScript
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'increment' });

useEffect Patterns

useEffect runs after render when dependencies change. Use it for synchronizing with external systems — subscriptions, timers, manual DOM APIs, and non-React widgets. Not for deriving state from props.

Always specify dependency arrays accurately — missing deps cause stale bugs; excessive deps cause loops. Return cleanup functions to unsubscribe and cancel timers. Async functions inside effects should not be the effect callback directly — define async inner function and call it.

  • Effects for external synchronization
  • Accurate dependency arrays
  • Cleanup on unmount
useEffect(() => {
  const controller = new AbortController();
  fetchData({ signal: controller.signal });
  return () => controller.abort();
}, [userId]);

useContext

Context passes values through the tree without prop drilling. Create with `createContext`, provide with `<Provider value={}>`, consume with `useContext`. Default values apply when no Provider exists — often null requiring guards.

Split contexts by update frequency — theme context changes rarely; auth session may change often. Mixing both in one context causes unrelated consumers to rerender. Memoize provider values to prevent new object references each render.

  • Avoid prop drilling
  • Split contexts by change frequency
  • Memoize provider value objects
const ThemeContext = createContext<Theme | null>(null);
const theme = useContext(ThemeContext);

useMemo and useCallback

useMemo caches computed values between renders when dependencies unchanged. useCallback caches function references. Both exist to optimize child components wrapped in React.memo or to stabilize effect dependencies.

Do not apply everywhere — memoization has cost. Profile first. Extract expensive computation to useMemo; pass stable callbacks to memoized children with useCallback. Dependencies must include every value referenced inside.

  • Profile before memoizing
  • useMemo for expensive calculations
  • useCallback for stable child props
const sorted = useMemo(() => sortItems(items), [items]);
const handleClick = useCallback(() => onSelect(id), [id, onSelect]);

Custom Hooks

Custom hooks extract reusable stateful logic: `function useDebounce(value, ms)`. They follow naming convention `use*` and may call other hooks. Share logic across components without HOCs or render props.

Test custom hooks with @testing-library/react renderHook. Keep hooks focused — one responsibility per hook. Compose small hooks into larger ones (`useUserSettings` built from useLocalStorage and useFetch).

  • Extract reusable stateful logic
  • Must follow Rules of Hooks
  • Test with renderHook
function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

Rules of Hooks

Call hooks only at the top level of function components or custom hooks — never inside loops, conditions, or nested functions. Call hooks only from React functions, not regular JavaScript functions.

ESLint plugin `eslint-plugin-react-hooks` enforces these rules. Violations cause state mismatch bugs that are extremely difficult to debug because hook order changes between renders.

  • Top level only — no conditionals
  • React functions only
  • eslint-plugin-react-hooks enforces rules

Get In Touch


Ready to discuss your next project? Drop me a message.