Reusing a network-status check:function useOnlineStatus() { return useSyncExternalStore(subscribe, () => navigator.onLine, () => true) } used by a save button and a status bar.
State hooks
useState
Store simple component state and update it with values or updater functions.
const [value, setValue] = useState(initial) — the setter schedules a re-render; it does not change value in the running code.
React skips the re-render when the new value is Object.is equal to the old one.
Objects and arrays must be replaced, not mutated: setItems([...items, item]).
Example: components/signup-form.tsx
"use client";
import { useState } from "react";
export function SignupForm() {
const [email, setEmail] = useState("");
const [agreed, setAgreed] = useState(false);
// Derived, not stored: it can never disagree with the fields.
const canSubmit = email.includes("@") && agreed;
return (
<form>
<input value={email} onChange={e => setEmail(e.target.value)} placeholder="you@example.com" />
<label>
<input type="checkbox" checked={agreed} onChange={e => setAgreed(e.target.checked)} /> I agree
</label>
<button disabled={!canSubmit}>Create account</button>
</form>
);
}
A shopping cart:dispatch({ type: 'added', product }) and dispatch({ type: 'removed', id }) keep every cart rule in one cartReducer.
useState lazy initialization
Compute an expensive initial state once by passing an initializer function.
useState(createTodos) passes the function; useState(createTodos()) calls it on every render.
React calls the initializer only during the first render.
Good for reading localStorage or building an expensive structure once.
Restoring a draft:useState(() => localStorage.getItem('draft') ?? '') reads storage once instead of on every keystroke.
useState functional updates
Use the previous state argument when multiple updates depend on the current value.
setCount(c => c + 1) uses the latest queued value, not the snapshot from this render.
Three setCount(count + 1) calls in one handler add 1; three updater calls add 3.
Updaters must be pure — React may call them twice in Strict Mode.
A double-click counter:onClick={() => { setLikes(l => l + 1); setLikes(l => l + 1); }} adds two likes reliably.
useReducer lazy initialization
Initialize reducer state through init and reset it predictably from an action.
useReducer(reducer, userId, createInitialState) calls the init function once with userId.
Reset by dispatching { type: 'reset', payload } and returning init(payload).
Avoid useReducer(reducer, createInitialState(userId)), which runs every render.
A form with a reset button: The reducer's reset case returns init(initialValues), restoring the original fields in one dispatch.
useReducer reducer purity
Keep reducers deterministic and free of network calls, mutations and other side effects.
A reducer receives (state, action) and returns new state — no fetch, timers or random values.
Copy before changing: return { ...state, items: [...state.items, action.item] }.
Strict Mode calls reducers twice to expose impurity.
Moving side effects out: Instead of saving inside the reducer, an Effect watches state.items and saves them to the server.
Effect hooks
useEffect
Synchronize a component with an external system after rendering.
It runs after the screen updates, then again whenever a dependency changes.
Return a cleanup function to undo the setup — React runs it before the next run and on unmount.
If there is no external system (network, DOM API, timer, socket), you probably do not need an Effect.
Example: lib/chat.ts
export function createConnection(roomId: string) {
const socket = new WebSocket(`wss://chat.example.com/rooms/${roomId}`);
return {
onMessage(handler: (text: string) => void) {
socket.addEventListener("message", e => handler(String(e.data)));
},
disconnect: () => socket.close(),
};
}
Connecting to a chat room:useEffect(() => { const c = createConnection(roomId); c.connect(); return () => c.disconnect(); }, [roomId]).
useLayoutEffect
Measure or update layout before the browser paints when a visual effect requires it.
It blocks the browser from painting until it finishes, so keep it short.
Use it to measure layout (e.g. getBoundingClientRect) and adjust before the user sees a flicker.
Prefer useEffect; switch only when you can see a visual jump.
Positioning a tooltip: Measure the tooltip's height in useLayoutEffect, then place it above the button if it would overflow the bottom of the screen.
useInsertionEffect
Insert styles before layout effects, primarily for CSS-in-JS library authors.
It runs before layout effects, so injected <style> rules exist when layout is measured.
It cannot update state, and refs are not attached yet when it runs.
Application code almost never needs it.
A CSS-in-JS library: The library's useCSS(rule) inserts the generated rule with useInsertionEffect so components never measure unstyled DOM.
useEffectEvent
Read fresh props and state from an Effect without making the Effect reactive to those values.
Logic inside an Effect Event reads the latest values but does not cause the Effect to re-run.
Leave it out of the dependency array — it is not reactive.
Split "what should re-sync" (Effect deps) from "what should only be read" (Effect Event).
Example: components/chat-room.tsx
"use client";
import { useEffect, useEffectEvent } from "react";
import { createConnection } from "@/lib/chat";
import { track } from "@/lib/analytics";
export function ChatRoom({ roomId, plan }: { roomId: string; plan: string }) {
const onConnected = useEffectEvent(() => {
track("room_joined", { roomId, plan }); // always the current plan
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on("connected", onConnected);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // plan is not a dependency
return <h2>Room {roomId}</h2>;
}
Logging a visit with the current cart size:const onVisit = useEffectEvent(url => log(url, cartCount)); useEffect(() => onVisit(url), [url]) — changing cartCount alone does not log again.
Hook dependency arrays
Declare every reactive value an Effect or memoized calculation reads from its scope.
List every prop, state and value declared in the component that the code reads.
No array runs after every render; [] runs once after mount.
To remove a dependency, change the code so it no longer reads it — never silence the linter.
Fixing a stale value: An Effect reading roomId with [] keeps the first room; adding [roomId] reconnects when the room changes.
useEffect cleanup
Disconnect subscriptions, remove listeners and abort work when dependencies change or components unmount.
Cleanup runs before every re-run of the Effect, not only on unmount.
Each setup should have a matching cleanup: subscribe/unsubscribe, connect/disconnect, add/remove listener.
Strict Mode runs setup → cleanup → setup in development to test this.
Disable duplicate navigation or show feedback while a transition is pending.
isPending is true until all updates in the transition have rendered.
Use it to dim content or disable a button rather than replacing the page with a spinner.
Updates in a transition do not show Suspense fallbacks for content already on screen.
Preventing double navigation: A tab button calls startTransition(() => router.push(href)) and shows isPending as a subtle loading bar.
useDeferredValue initialValue
Provide a first deferred value for a loading or filtered view during the initial render.
useDeferredValue(value, initialValue) renders initialValue first, then the real value in the background.
Useful when the first render should be fast, e.g. an empty list before results.
Added in React 19.
Fast first paint for search results:useDeferredValue(query, '') shows an empty result list immediately, then fills it once React has time.
Async hooks
use
Read a Promise or context value during rendering and coordinate it with Suspense.
use(promise) suspends the component until the promise resolves and shows the nearest <Suspense> fallback.
Unlike other Hooks it can be called inside if and loops.
Promises should come from a Server Component or a cache — not be created during the client render.
Example: app/posts/[id]/page.tsx
import { Suspense } from "react";
import { Comments } from "./comments";
import { getPost, getComments } from "@/lib/data";
export default async function PostPage({ params }: PageProps<"/posts/[id]">) {
const { id } = await params;
const post = await getPost(id); // needed for the first paint
const commentsPromise = getComments(id); // streamed in later
return (
<article>
<h1>{post.title}</h1>
<Suspense fallback={<p>Loading comments…</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</article>
);
}
Streaming comments into a post: The server passes commentsPromise down; <Comments> calls use(commentsPromise) inside <Suspense fallback={<Spinner />}>.
use with promises
Read a stable promise, suspend while pending and handle rejection with an Error Boundary.
Create the promise in a Server Component or cache it, so re-renders reuse the same promise.
A rejected promise goes to the nearest error boundary.
Call use inside <Suspense> to show a fallback while it is pending.
Loading a user profile:const user = use(userPromise) inside <Suspense fallback={<Skeleton />}>, with an error boundary showing "Couldn't load profile".
use with conditional context
Read context conditionally or in a loop where useContext's top-level rule would be restrictive.
use(Context) can be called after an early return, unlike useContext.
It reads from the closest provider, same as useContext.
This avoids reading context in components that bail out early.
An optional admin panel:if (!isAdmin) return null; const perms = use(PermissionsContext); reads permissions only for admins.
External store hooks
useSyncExternalStore
Subscribe to an external store with consistent snapshots across client and server rendering.
Following browser online status:useSyncExternalStore(subscribe, () => navigator.onLine, () => true) with subscribe listening to online and offline events.
useDebugValue
Label custom Hook state in React DevTools without changing runtime behavior.
useDebugValue(isOnline ? 'Online' : 'Offline') inside a custom Hook shows in React DevTools.
It is only useful in shared custom Hooks.
It has no effect in production.
Labelling a shared Hook: Inspecting a component in DevTools shows OnlineStatus: "Offline" next to the Hook.
useSyncExternalStore server snapshot
Supply a stable server snapshot so external-store content hydrates consistently.
The third argument returns the value to use during server rendering and hydration.
It must be the same on server and client during hydration.
Without it, server rendering throws for that component.
Online status with SSR:getServerSnapshot = () => true assumes online on the server, then updates on the client if the user is offline.
useSyncExternalStore browser subscriptions
Subscribe and unsubscribe to an external source without tearing during concurrent rendering.
subscribe(callback) must return an unsubscribe function.
React calls callback to re-check getSnapshot whenever the store reports a change.
It prevents "tearing" — two parts of the screen showing different store values.
Tracking window width: Subscribe to resize and return window.innerWidth as the snapshot to drive a responsive layout.
useDebugValue formatter
Format custom Hook labels lazily so development diagnostics do not add production work.
Pass a second argument: useDebugValue(date, d => d.toDateString()).
The formatter runs only when DevTools inspects the component.
Use it when formatting is expensive.
Formatting a timestamp label: A useLastSaved Hook shows Saved: Tue Oct 14 in DevTools without formatting on every render.
Utility hooks
useId
Generate stable unique IDs for accessibility relationships across server rendering and hydration.
The ID is the same on server and client, so it does not cause hydration mismatches.
Do not use it for list keys — keys should come from your data.
Call it once and add suffixes for related elements: ${id}-hint.
Example: components/password-field.tsx
"use client";
import { useId } from "react";
export function PasswordField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Password</label>
<input id={id} type="password" aria-describedby={`${id}-hint`} />
<p id={`${id}-hint`}>At least 12 characters.</p>
</div>
);
}
An accessible form field:const id = useId(); <label htmlFor={id}>Email</label><input id={id} aria-describedby={${id}-hint} />.
useId accessibility
Connect labels, descriptions and controls with deterministic IDs rather than random values.
Connect <label htmlFor>, aria-describedby and aria-labelledby with generated ids.
Each component instance gets its own id, so reused fields never clash.
identifierPrefix on the root avoids clashes between several React roots on one page.
A password field with a hint:<input aria-describedby={hintId} /> <p id={hintId}>At least 12 characters</p> reads the hint aloud in screen readers.
Performance hooks
useMemo
Cache a calculation when measurement shows recomputation is costly.
useMemo(() => compute(a, b), [a, b]) recomputes only when a dependency changes.
Measure first — most calculations are fast enough without it.
It also keeps an object's identity stable for a memo child or an Effect dependency.
Filtering a large list:const visible = useMemo(() => todos.filter(t => matches(t, filter)), [todos, filter]) skips re-filtering when only the theme changes.
useCallback
Cache a function identity when it matters to memoized children or Hook dependencies.
useCallback(fn, deps) is useMemo(() => fn, deps).
It helps only when the function goes to a memo child or is used as a Hook dependency.
Use the updater form inside to drop state from the deps: setTodos(t => [...t, item]).
A memoized list row:const onSelect = useCallback(id => setSelected(id), []) lets each memo(Row) skip re-rendering when the parent updates.
useMemo and the React Compiler
Know when automatic memoization makes manual useMemo and useCallback unnecessary.
The compiler memoizes values and functions automatically at build time.
Existing useMemo keeps working; remove it gradually where the compiler covers it.
Keep manual memo when a value must be stable as an Effect dependency.
Simplifying a codebase: After enabling the compiler, a team removes useCallback wrappers from event handlers without measurable slowdowns.
useMemo cache semantics
Treat memoization as a performance optimization, never as storage for required application state.
React may throw away cached values (e.g. offscreen components), so code must work without the cache.
Never store data you would lose — use state for that.
The calculation must be pure.
A safe memoized sort:useMemo(() => [...rows].sort(byDate), [rows]) copies before sorting so the original array is never mutated.
useCallback and memo
Use callback identity with memoized children only when it prevents measured work.
memo(Child) skips re-rendering only if every prop is equal, functions included.
Without useCallback, a new function each render defeats memo.
Check with the React Profiler that the child was actually slow.
A big table with row actions: A stable onDelete via useCallback lets 1,000 memo(Row) components skip re-rendering when a filter changes elsewhere.
Testing Hooks
Hook testing
Test Hook behavior through a user-facing component rather than coupling tests to private implementation.
Test what the user sees: render a component, click, assert the screen.
Mock the network (e.g. MSW), not the Hook itself.
Wrap asynchronous updates in await with findBy* queries.
Testing a counter Hook: Render <Counter />, click "Add" twice with user.click, then expect the text 2.
Hook patterns
Hooks and StrictMode
Development mounts, unmounts and remounts a component to surface impure renders and missing cleanup.
In development it renders twice, runs Effects twice and calls ref callbacks twice.
None of this happens in production.
If something breaks on the second run, add or fix the cleanup.
Finding a missing unsubscribe: Duplicate chat messages in dev reveal that the Effect never removed its socket listener.
eslint-plugin-react-hooks
Let the lint rules enforce call order and dependency completeness instead of reviewing both by eye.
rules-of-hooks blocks Hooks in conditions and loops.
exhaustive-deps warns about missing Effect, useMemo and useCallback dependencies.
Newer versions also include React Compiler rules such as purity checks.
Catching a bug in review: The linter flags useEffect(() => fetchUser(id), []) with "missing dependency: id" before it ships.
renderHook in tests
Exercise a hook through a test renderer rather than through a component written only for the test.
renderHook(() => useCounter()) from Testing Library returns { result }.
Wrap updates in act, and pass wrapper to provide context providers.
Prefer component tests unless the Hook is a reusable library API.
Testing a shared Hook:const { result } = renderHook(() => useToggle()); act(() => result.current.toggle()); expect(result.current.on).toBe(true).
AbortController in useEffect
Cancel an in-flight request from the cleanup function so a superseded response cannot win.
Create the controller inside the Effect and pass signal to fetch.
Call controller.abort() in the cleanup function.
Ignore the AbortError it causes, since it is expected.