Use TanStack Query (or RTK Query / SWR) over hand-rolled `useEffect + fetch`. Handle the four states (idle, loading, success, error) explicitly. Distinguish error classes: network, 4xx (user / validation), 5xx (server), abort. Retry transient errors with backoff; don't retry 4xx. Show inline errors per query and a global error boundary for unexpected crashes. Cancel stale requests on dependency change. Always revalidate on the server.
Don't roll your own. Use a CRDT (Yjs) or OT engine for conflict-free concurrent edits, a rich-text framework (ProseMirror via TipTap / Slate / Lexical) for the editor, and WebSocket / WebRTC transport with awareness for cursors. Render presence + remote cursors via decorations. Persist server-side. CRDTs are dominant in 2026 for offline-tolerance and decentralized topologies; OT for centralized servers like Google Docs.
A correct fetch hook tracks {status, data, error}, cancels in-flight requests on unmount or arg change with AbortController, and avoids stale-state-after-unmount warnings. The reducer pattern keeps transitions safe.
Mirror useState but read initial value from localStorage and write on change. Handle JSON errors, SSR (no window), quota exceeded, and listen to the storage event for cross-tab sync.
Predictable re-renders: colocate state so changes have a narrow blast radius; memoize children with React.memo + stable callback identity (useCallback/useMemo for props); split contexts so a frequently-changing slice doesn't re-render unrelated consumers; for hot state, use external stores (Zustand) with selector-based subscriptions; profile with DevTools to verify before optimizing.
When the same endpoint is hit repeatedly (search-as-you-type, paging), responses can land out of order. The fix is take-latest: abort the previous request with AbortController, or guard with a request-id ref so only the newest response updates state.
Context for low-frequency cross-cutting values (theme, locale). Zustand for medium app-wide state with selector-based subscriptions. Redux Toolkit when you need devtools, time-travel, and a strict update protocol.
Controlled = React state owns the value (`value={x}` + `onChange`). Uncontrolled = DOM owns the value, read via `ref.current.value` or on submit. Controlled wins for live validation, conditional logic, complex forms. Uncontrolled is faster and simpler for plain forms that just submit once. Don't mix the two on the same input.
Use CSS custom properties for color tokens; toggle a `class="dark"` or `data-theme` attribute on `<html>` to flip them. Read user preference from localStorage, fall back to `prefers-color-scheme`. Set the class **before first paint** (inline script in `<head>` for SSR) to avoid a light→dark flash. Tailwind: `darkMode: "class"`. Persist and broadcast changes across tabs.
Error boundaries catch render errors in the subtree below them and show a fallback. Place one at the app root (last line of defense), one per route, and one per significant widget (chart, table, embed) so one crash doesn't take down the page. Pair with global window error handlers for unhandled promise rejections, and a logger (Sentry) to capture stack + componentStack + user context.
React wraps native DOM events in a SyntheticEvent for cross-browser consistency. Attach handlers as JSX props (`onClick`, `onChange`) — camelCase, function reference, not a string. Since React 17, events are delegated to the React root, not `document`. Call `e.preventDefault()` / `e.stopPropagation()` on the synthetic event. For some events React doesn't expose (focus visible, native scroll passive listeners), use `addEventListener` in a `useEffect`.
Fiber is React's reconciler: a linked-list tree of work units that can be paused, resumed, and prioritized. It's what unlocked concurrent rendering, Suspense, and transitions.
For non-trivial forms, use `react-hook-form` + `zod`. RHF uses uncontrolled inputs under the hood (no re-render per keystroke), Zod owns the schema (single source of truth for types + runtime validation). Validate on blur for individual fields and on submit for the whole form. Accessibility: associate errors to fields with `aria-describedby`; focus the first invalid field on submit failure. Always re-validate on the server.
Functional components are the modern default — hooks replaced class lifecycle methods, with cleaner composition and smaller bundles. Classes still appear in legacy code and ONE place hooks can't reach: error boundaries (componentDidCatch).
A HOC is a function that takes a component and returns an enhanced one (`withFoo(Component)`). Useful for cross-cutting concerns: auth, logging, theming, data fetching. Mostly superseded by hooks (cleaner, no prop-namespace pollution, no 'wrapper hell'). Still appropriate for: HOCs that operate at the component-tree level (auth gating, error boundaries with HOC API, code splitting).
Concurrent rendering lets React prepare multiple UI versions in the background. `useTransition` marks a state update as non-urgent so React can interrupt it for higher-priority work like typing.
Lift shared state to the closest common ancestor and pass down. For deep trees, use Context (small slices) or a tiny store like Zustand. Don't use refs/imperative handles unless you really mean to escape React's data flow.
Hydration is the client running React to attach handlers to server HTML. In complex apps: prevent mismatches (same data, same time, same flags on server + client); code-split + lazy-hydrate heavy below-the-fold parts; use Suspense + streaming so hydration is incremental; use RSC to skip hydration for non-interactive UI; defer non-critical to `requestIdleCallback`. Mismatches usually mean impure render.
Wrap the app in a router, declare routes (path → element), navigate with `<Link>` / `useNavigate`, read params via `useParams`/`useSearchParams`. v6+ supports nested routes, loaders, and data routers.
`useOptimistic(state, reducer)` returns `[optimisticState, addOptimistic]`. Call `addOptimistic(action)` inside a transition (typically before `await api.submit()`); the UI shows the optimistic state immediately; once the underlying state updates (or the transition finishes), `optimisticState` reverts to derived-from-real. Rollback on error is automatic. Pairs with Server Actions in React 19.
Toast manager: context/store of active toasts, a `<ToastContainer/>` portal that renders them stacked, imperative API (`toast.success(msg)`) backed by the store, per-toast options (variant, duration, dismissible, action), auto-dismiss timers, queueing if max-visible exceeded, swipe-to-dismiss, focus management, ARIA live region for accessibility.
`useStorage(key, initial, storage)`: lazy init from storage (parse JSON, fallback to initial); useState pair; useEffect writes to storage when value changes; listen to `storage` event to sync across tabs; handle SSR (no `window` on server) by skipping storage on first render; handle parse errors and quota.
Use IntersectionObserver on a sentinel element to trigger the next page fetch. Track cursor + loading + hasMore in state, dedupe in-flight requests, and virtualize once rendered rows exceed a few thousand.
Slice state by widget, use selectors with referential stability, isolate live-update components behind their own subscriptions, and memoize where measurement justifies it.
Two flavors: `useDebouncedValue(value, delay)` returns the latest value after the input has been stable for `delay` ms — built with `useState` + `useEffect` setTimeout cleanup. `useDebouncedCallback(fn, delay)` returns a stable function that delays its invocation — built with `useRef` for the timer and `useRef` for the latest fn so closures stay fresh.
Render only the slice of rows within the scroll viewport plus overscan. Maintain total scroll height via a spacer or absolutely-positioned container. For 100k rows, fixed heights are simplest; dynamic heights require measurement caches. Use `@tanstack/react-virtual` in production; for an interview, code it from scratch using a scroll listener + visible-range math.
Build a11y into the design system, not into individual screens. Use semantic HTML + accessible primitives (Radix, React Aria), enforce with linting (eslint-plugin-jsx-a11y) and automated audits (axe in CI + Lighthouse), test with keyboard + a real screen reader (NVDA/VoiceOver), and own metrics like keyboard coverage and contrast pass rate. Treat a11y like a feature with a budget and a tracking dashboard, not a pre-launch checklist.
Build a 3x3 grid with turn tracking, win/draw detection, and reset. Surface state shape, win-line generation, immutability, and how to extend to NxN as the senior signal.
Two viable models: (1) Snapshot stacks — store past/present/future state snapshots; on undo, pop past → present and push old present to future. Cheap with structurally-shared state (Immer). (2) Command pattern — store reversible operations (`apply`/`invert`). Better for huge state (drawings) where snapshots are expensive. Coalesce rapid changes (typing) into one history entry; cap history depth.
Memoization adds bookkeeping cost for every render. It only pays off when the work is expensive AND the deps are actually stable AND a downstream consumer cares about identity. Most of the time it makes code noisier without measurable wins.
Optimistic = update UI immediately, send the request, roll back on failure. Pessimistic = show a loading state, update only on success. Optimistic feels faster but needs rollback paths and clear error UX. Use optimistic for high-confidence, low-stakes mutations (likes, toggles, list reorders); pessimistic for irreversible or expensive ops (payments, deletions, bulk actions).
Props are inputs passed in from a parent — read-only inside the component. State is internal, mutable via `setState`. Props flow down; events flow up. If multiple components need the same value, lift it to the lowest common parent and pass it down as a prop. If a piece of data can be derived from props, don't put it in state.
Wrap protected routes in a guard component that reads auth state from a context/store and either renders the children, redirects to `/login` (with a `?next` param so post-login lands on the requested page), or renders a loading state during the auth probe. For role-based access, wrap a second guard around routes that require specific roles. The frontend guard is **UX**, not security — every protected API endpoint must enforce auth server-side independently.
React 18+ can render in the background, interrupt itself, and prioritize urgent updates. The primitives: `useTransition` / `startTransition` (mark non-urgent updates), `useDeferredValue` (lag a value to keep input responsive), Suspense for data + code streaming, and automatic batching across async boundaries. They don't make React faster — they let you schedule work so that user input always wins.
Every consumer of a context re-renders whenever the provider's `value` changes by reference. Stabilize the value, split contexts, or use a selector library (Zustand, use-context-selector) for high-churn state.
Keys give React identity for siblings in a list. Without stable keys, React matches children by position — reordering or inserting mid-list causes wrong component reuse: state, DOM, refs, and focus follow the slot, not the data. Use a stable, unique id from the data; never the array index unless the list is append-only and uneditable.
RSC are React components that run only on the server, render to a special serialized format streamed to the client, and never ship their code or dependencies to the browser. Mix with Client Components (`"use client"`) for interactivity. Benefits: zero JS for static parts, direct DB access, secrets stay server-side, automatic streaming. Trade-off: a new mental model — you can't pass functions or class instances across the boundary.
React renders an in-memory tree, diffs it against the previous one with O(n) heuristics (same type = update props; different type = replace; keys identify list items), then commits the minimal DOM mutations.
Scenario Based: You have an e-commerce product listing where multiple users can add items to cart simultaneously. How would you use React Query / SWR with optimistic updates to prevent stale UI
Scenario Based: You're designing a dashboard where chart updates, user notifications, and data fetches must happen independently. How would you use React Context / Custom Hooks to achieve this
A useCallback with `[]` captures state from the first render. setCount(count + 1) keeps using 0; setCount(prev => prev + 1) always reads the latest. Prefer functional updates whenever the new state depends on the previous.
React wraps native DOM events in a cross-browser `SyntheticEvent` shim. Same API (`preventDefault`, `stopPropagation`, `target`) but normalized. React 17+ attaches listeners at the **root container**, not `document`; events bubble up through React's tree. Differences from native: no event pooling since React 17; `onChange` fires on every keystroke; `onScroll` doesn't bubble; some events use capture phase.
`useEffect` runs after the browser paints — async, doesn't block visual updates. `useLayoutEffect` runs synchronously after the DOM mutates but BEFORE paint — use it when you need to measure layout and mutate the DOM before the user sees anything. Default to `useEffect`; reach for `useLayoutEffect` only for measure-and-adjust patterns (tooltips, animations from a measured position).
`useMemo(fn, deps)` caches the *return value* of `fn`. `useCallback(fn, deps)` caches the *function itself*. `useCallback(fn, d)` is exactly `useMemo(() => fn, d)`. Use them for referential stability of values/functions passed to memoized children, and for genuinely expensive computations. In React 19+, the React Compiler handles most of these automatically.
`useRef` returns a mutable container that survives renders without triggering them. `forwardRef` lets a parent's ref reach a child's DOM node. Use refs for imperative DOM access and persistent values; never as state replacements.
useState is direct value-replacement, ideal for independent primitives or small objects. useReducer centralizes complex transitions in a pure function, ideal when next-state depends on the action *and* current state in non-trivial ways.
React.lazy turns a dynamic import into a component; Suspense renders a fallback while the chunk loads. Together they split bundles at component granularity without ejecting from React's render model.
An HOC is a function that takes a component and returns a new component, wrapping it with extra props or behavior. Hooks have largely replaced HOCs for state/logic reuse, but HOCs still shine for cross-cutting wrappers like auth gates, error boundaries, and analytics.
SSR returns server-rendered HTML so the user sees content immediately — better FCP/LCP, better SEO, faster first paint on slow devices, link-preview support (Open Graph). Trade-offs: origin cost per request, hydration (a long task), TTFB depends on server work. Best fit: content-heavy or SEO-critical pages. Streaming SSR + RSC mitigate the costs.
Hydration mismatches happen when server-rendered HTML doesn't match the client's first render. Common causes: time/locale, random IDs, browser-only APIs, third-party DOM mutations.
Hydration: the client runs React on the same component tree the server already rendered, attaching event handlers and re-running effects, but **reusing existing HTML**. Mismatches happen when server and client render different output — caused by time/dates, browser-only APIs (`window`), random values, feature flags differing, or `useEffect`-style state read during render. Use `useId`, guard browser globals, serialize server data, or render client-only via dynamic imports.
Refs for values that need to persist across renders but **don't drive rendering**: DOM nodes, timer/interval ids, mutable counters, latest-value mirrors for stale-closure fixes, third-party instances. State for values that, when changed, must trigger a re-render. Mutating a ref doesn't re-render; setting state does. Don't read/write refs during render — only in effects and handlers.
StrictMode intentionally double-invokes components, effects, and reducers in development to surface impure renders and effect cleanup bugs that would break under concurrent rendering.