Topics
v5.0.0
On this page · sections

Hooks & Refs

Hooks let function components hold state, side effects, and imperative handles. useRef is the escape hatch when a value must survive re-renders without triggering them.

APITriggers re-renderPrimary use
useStateYesData the user should see update
useReducerYesComplex state transitions, stable dispatch
useRefNoDOM nodes, timers, instance handles
Custom hooksDepends on internalsReusable stateful logic
forwardRef / ref propNoExpose DOM ref from wrapper components

useState

Definition

useState returns a value and a setter. Calling the setter schedules a re-render with the new value.

'use client'; import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <button type="button" onClick={() => setCount((c) => c + 1)}> Count: {count} </button> ); }

Functional updates (setCount(c => c + 1)) read the latest state — important inside closures and batched updates. The setter identity is stable and need not appear in effect dependency arrays.

Lazy initialization

const [data, setData] = useState(() => expensiveParse(rawInput));

The initializer runs once on mount. Use it when the initial value is costly to compute.

Interview: If the user should see a value change on screen, use state. State updates are asynchronous and batched in React 18+.

useReducer

Definition

useReducer manages state through a pure reducer function — ideal when the next state depends on the previous state and action type.

'use client'; import { useReducer } from 'react'; type State = { count: number }; type Action = { type: 'inc' } | { type: 'dec' } | { type: 'reset' }; function reducer(state: State, action: Action): State { switch (action.type) { case 'inc': return { count: state.count + 1 }; case 'dec': return { count: state.count - 1 }; case 'reset': return { count: 0 }; default: return state; } } function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <> <span>{state.count}</span> <button type="button" onClick={() => dispatch({ type: 'inc' })}>+</button> </> ); }

dispatch identity is stable — safe to pass through Context without useCallback. Prefer useReducer when state logic grows beyond a few setState branches or when splitting state/dispatch contexts.

Interview: useReducer is not faster than useState — it organizes transitions. Dispatch is stable; setters from useState are also stable.

useRef vs useState

Definition

useRef returns a stable object { current: T } for the component’s lifetime. Mutating .current is synchronous and does not schedule a re-render.

'use client'; import { useRef, useState } from 'react'; function Counter() { const renderCount = useRef(0); const [count, setCount] = useState(0); renderCount.current += 1; return ( <div> <p>Count: {count}</p> <p>Renders: {renderCount.current}</p> <button type="button" onClick={() => setCount((c) => c + 1)}> Increment </button> </div> ); }
State → for rendering Ref → for remembering

Reading ref during render is fine. Writing during render is discouraged in concurrent mode — two renders can interleave writes. Write in events or effects.

Interview: Hidden UI state in a ref produces a stale screen.

DOM Refs

Definition

Attach a ref to a DOM element to get imperative access after commit:

'use client'; import { useEffect, useRef } from 'react'; function SearchField() { const inputRef = useRef<HTMLInputElement>(null); useEffect(() => { inputRef.current?.focus(); }, []); return <input ref={inputRef} type="search" />; }

Ref attachment happens in the commit phase. Until then, ref.current may be null — guard with optional chaining.

Render → Commit (attach refs) → useLayoutEffect → Paint → useEffect

Interview: Use a ref, not document.querySelector. It breaks SSR, portals, tests, and encapsulation.

Mutable Values Without Re-renders

Definition

Common non-DOM ref use cases:

Timer IDs, interval handles → ref + cleanup in useEffect AbortController → ref or local variable in effect Previous prop value → update ref in effect after read Latest callback → callbackRef.current = fn each render

Previous value pattern

function usePrevious<T>(value: T): T | undefined { const ref = useRef<T | undefined>(undefined); useEffect(() => { ref.current = value; }, [value]); return ref.current; }

Latest callback pattern

const callbackRef = useRef(onTick); callbackRef.current = onTick; useEffect(() => { const id = setInterval(() => callbackRef.current(), 1000); return () => clearInterval(id); }, []);

Do not put ref.current in effect dependency arrays — changing .current does not trigger re-runs.

Interview: Prefer AbortController over isMounted ref guards for fetch cleanup.

Ref Forwarding

Definition

In React 19, function components accept ref as an ordinary prop. On React 18 and earlier, wrap with forwardRef:

'use client'; import { useImperativeHandle, useRef, type Ref } from 'react'; type TextFieldProps = { label: string; ref?: Ref<HTMLInputElement> }; function TextField({ label, ref }: TextFieldProps) { return ( <label> {label} <input ref={ref} type="text" /> </label> ); }

useImperativeHandle exposes a custom API instead of the raw DOM node — useful for widget-like components (maps, editors, video players).

Interview: Default to exposing DOM. Custom handles only when the parent needs intentional imperative methods.

Custom Hooks

Definition

Custom hooks extract reusable stateful logic into composable functions. They follow the Rules of Hooks: only call at top level, only in React functions.

'use client'; import { useEffect, useState } from 'react'; type Viewport = { width: number; height: number }; export function useViewport(): Viewport { const [viewport, setViewport] = useState<Viewport>(() => ({ width: typeof window !== 'undefined' ? window.innerWidth : 0, height: typeof window !== 'undefined' ? window.innerHeight : 0, })); useEffect(() => { function update() { setViewport({ width: window.innerWidth, height: window.innerHeight }); } window.addEventListener('resize', update); return () => window.removeEventListener('resize', update); }, []); return viewport; } function ResponsiveBanner() { const { width } = useViewport(); return <header>{width < 640 ? 'Mobile' : 'Desktop'} header</header>; }

Each call site has independent state unless backed by Context. Hooks share logic, not implementation.

Context + hook pattern

const UserContext = createContext<{ id: string } | null>(null); function useUser() { const ctx = useContext(UserContext); if (!ctx) throw new Error('useUser requires UserProvider'); return ctx; }

Interview: Hooks do not replace Context — useTheme() reads context. A hook returning JSX is an anti-pattern; return data and callbacks.

Imperative Third-Party Integration

Definition

The ownership rule:

React owns every node it renders children into → Manual writes to those nodes are undone on next commit Sanctioned pattern → Empty host div via ref → Library owns children inside the host → React reconciles zero children in that subtree

Real-world example

'use client'; import { useEffect, useRef } from 'react'; function MapHost({ center }: { center: [number, number] }) { const hostRef = useRef<HTMLDivElement>(null); const mapRef = useRef<MapInstance | null>(null); useEffect(() => { mapRef.current = createMap(hostRef.current!); return () => { mapRef.current?.destroy(); mapRef.current = null; }; }, []); useEffect(() => { mapRef.current?.setCenter(center); }, [center]); return <div ref={hostRef} />; }

Mount effect has empty deps — instance survives prop changes. Second effect pushes updates through the library’s API.

Interview: Give every imperative wrapper a documented ownership boundary and enforce it in review.

Senior Interview Summary

useState

Schedules re-render when value changes. Use functional updates in closures. Lazy init for expensive defaults.

useReducer

Pure reducer + stable dispatch. Split state/dispatch contexts for performance.

useRef

Stable { current } object. Mutating .current does not re-render.

Custom hooks

Share logic, not implementation. Top-level only. Return data and callbacks — not JSX.

DOM ref

Attached at commit. null until mounted and after unmount.

Ownership rule

React owns the host element; the library owns its children inside an empty host div.

Best mental model

useState → "Show this on screen." useReducer → "Transition state via actions." useRef → "Remember this across renders." custom hook → "Extract reusable stateful logic." host div → "React reconciles nothing inside here."

Practical rule

Visible UI state → useState or useReducer Timer / abort handle → ref + effect cleanup Third-party widget → mount once, update via API, destroy in cleanup App logic reuse → custom hook (test with renderHook) Focus on mount → useEffect or event handler

Last updated on