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.
| API | Triggers re-render | Primary use |
|---|---|---|
useState | Yes | Data the user should see update |
useReducer | Yes | Complex state transitions, stable dispatch |
useRef | No | DOM nodes, timers, instance handles |
| Custom hooks | Depends on internals | Reusable stateful logic |
forwardRef / ref prop | No | Expose 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:
useReduceris not faster thanuseState— it organizes transitions. Dispatch is stable; setters fromuseStateare 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 rememberingReading 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 → useEffectInterview: 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 renderPrevious 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
AbortControlleroverisMountedref 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 subtreeReal-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.currentdoes not re-render.
Custom hooks
Share logic, not implementation. Top-level only. Return data and callbacks — not JSX.
DOM ref
Attached at commit.
nulluntil 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