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.
Principles: one responsibility per component, separate UI from logic (custom hooks), props as the public API (small + named), state colocated where used, derive don't store, lift state only when necessary, names that describe purpose not implementation. Avoid prop drilling > 2 levels (compose, context, or store). Test by behavior, not implementation.
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.
Track an ordered array of activated cell ids in state. On click, if not active, push; if already active, ignore (or toggle off). Once all activated, deactivate one-by-one (setInterval) in reverse order via .pop(). Render: each cell highlights if its id is in the array. Clean up the interval on unmount and when the array empties.
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.
Patterns: `&&` for show-or-hide, ternary for either-or, early return for guards, switch/lookup for many cases, render prop for conditional content with shared logic, Suspense/ErrorBoundary for loading/error. Avoid `&&` with a numeric `0` (renders "0"). Hoist branches into named components when JSX gets dense.
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`.
Concurrent rendering lets React pause/resume/abandon renders. Key features: automatic batching across async boundaries, `startTransition` / `useTransition` for non-urgent updates, `useDeferredValue` for lagging expensive derivations, Suspense for data + lazy components, streaming SSR with selective hydration. The model: urgent updates (input) preempt non-urgent (large list re-renders).
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.
Separate kinds of state first: server state → React Query; local UI → useState; cross-tree config (theme, auth) → Context; cross-tree app state (cart, editor) → Zustand or Redux Toolkit with selector subscriptions; URL state → router. With multiple teams: shared store package, feature-scoped slices, RFCs for additions, store namespacing to prevent collisions, devtools, and clear ownership.
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).
Yes — Jest/Vitest as the runner, Testing Library for components (queries by role, behavior over implementation). Mock fetch with MSW for API flows (intercept network, no `fetch` mocks). Test what the user sees: roles, accessible names, behavior on click/type. Avoid testing internal state or implementation details. Coverage on critical paths; E2E (Playwright) for golden flows.
Yes — but selectively. `React.memo` for genuinely expensive children with stable prop references. `useCallback` for callbacks passed to memo'd children or as effect deps. `useMemo` for measurable expensive derivations. Other levers: virtualization, code splitting, `useDeferredValue`/`startTransition`, Suspense, Server Components. Profile first; the default of nothing is right most of the time.
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.
Start with RUM (web-vitals: LCP, INP, CLS, p75 in prod). Reproduce in dev → React DevTools Profiler shows which components rendered, why, and how long. Chrome Performance panel for main-thread tasks. Lighthouse for asset/CSS issues. Memory panel for leaks. Bundle analyzer for size. Don't optimize without measurement.
React DevTools Profiler measures component render time and shows what re-rendered and why. Chrome Performance panel shows main-thread tasks including style, layout, paint, composite — browser work. If Profiler shows fast renders but the user sees jank, the browser's layout/paint is the culprit. If Profiler shows slow renders, optimize React (memo, lists, context). Pair the two tools.
Pair every subscription with cleanup: `useEffect` returns a teardown; remove listeners; clear intervals; abort fetches with AbortController; unsubscribe from stores. Avoid holding refs to large objects in long-lived closures. Bound caches (LRU). Detect with DevTools Memory snapshots (detached DOM, retained size). Strict Mode helps surface missing cleanup in dev.
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.
Since React 17, React attaches one listener per event type at the **app root** (the container you passed to `createRoot`), not at `document`. Events bubble up; React's synthetic event system reconstructs the event, walks the React tree, and fires the appropriate `onClick` handlers. `onClickCapture` runs in capture phase. `stopPropagation` on a synthetic event only stops React listeners, not native ones.
Two phases: **render** (call components, build a fiber tree, diff against previous) and **commit** (apply DOM changes). Render is interruptible in concurrent mode; commit is synchronous. Each component instance is a Fiber node with state, hooks, and references. Reconciliation pairs elements by type + key; same type → update, different → unmount + mount.
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.
Tokens as CSS variables under `[data-theme]` or a class on `<html>`. Resolve theme on mount from localStorage or `prefers-color-scheme`. Apply before first paint via inline `<script>` to avoid flash. Provide a React context for components to read the current theme. Persist user choice. Respect `prefers-color-scheme: dark` as default. Support system / light / dark trichotomy.
Hydrate persisted state in a useEffect (not at render) to avoid SSR mismatches. Wrap JSON.parse in try/catch; treat parse failure as 'no saved state'. Catch QuotaExceededError on writes and degrade (clear oldest, warn user, switch to in-memory). Validate persisted shape with a schema (Zod) and migrate or discard on version mismatch.
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.
Wrap routes with an `<RbacRoute>` that checks user role before rendering. Lazy-import the protected component inside that check so unauthorized users never download it. Server should also enforce RBAC (don't trust the client). For finer control, gate per feature with role-aware lazy imports and a permission hook (`usePermission(role)`).
`React.lazy(() => import('./Page'))` defers the chunk; wrap with `<Suspense fallback={<Skeleton />}>`. Compose with route protection: check auth first, then lazy-render the protected component. Use `<ErrorBoundary>` to handle chunk-load failures (network blip). Prefetch on link hover to mask first-visit latency.
Virtualize (TanStack Virtual / react-window) — render only visible rows. Memoize the row component. Stable keys. Avoid layout reads in row render. Defer non-urgent updates with `useDeferredValue` or `startTransition`. For variable heights use measurement cache. Server-side filter/sort for huge datasets so the client only handles a window. Don't memoize the list itself — focus on rows.
Slice state by widget, use selectors with referential stability, isolate live-update components behind their own subscriptions, and memoize where measurement justifies it.
`useDebouncedValue`: returns a value that updates only after the input has stopped changing for N ms. Implement with `useState` + `useEffect` (set timer on value change, clear on cleanup). Useful for search-as-you-type. Distinct from `useDeferredValue` (React 18) which is concurrent-mode aware and yields to interaction.
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.
Render props: a component takes a function as a child (or prop) and calls it with internal state. `<Mouse>{({x, y}) => <p>{x}</p>}</Mouse>` — the consumer decides what to render. Pre-hooks pattern for sharing logic. Mostly replaced by custom hooks; still useful for inversion of control (forms, drag, headless UI).
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.
Behavioral: pick a real case where the optimization had no measurable effect or backfired. Common stories: over-memoization (useMemo/useCallback adding cost > benefit), virtualization on a too-small list, premature code splitting causing chunk waterfalls, debouncing the wrong handler. The key signal is showing you measured, learned, and reverted.
`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.
Big initial bundle, hydration cost, re-rendering large trees on every state change (no memo, context misuse), expensive renders (heavy list rendering without virtualization, recomputation per render), long synchronous work in handlers (INP), image-heavy LCP, unnecessary effects, memory leaks (no cleanup). Most are addressable with profiling + targeted memoization, virtualization, RSC, and transitions.
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.
Portals render a child into a DOM node outside the parent's hierarchy while preserving React's component tree (state, context, events). Used for modals, tooltips, popovers, toasts — anything that needs to escape `overflow:hidden` or stacking context but stay logically inside the component. Events still bubble through React's virtual tree, not the DOM ancestor.
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.
Common causes: parent re-render cascading, unstable prop references (new object/array/function each render), context value changing every render, hot state in Context, missing memoization for expensive children. Fixes: keep prop references stable (useMemo/useCallback when memo is downstream), split context, lift hot state to a selector-based store, React.memo for genuinely heavy children. Profile before applying.
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.