A **Component** is the function or class you write (`function Button(){...}`). A **React Element** is a plain object describing what to render — what `<Button />` evaluates to: `{ type: Button, props: {...}, key, ref }`. A **React Node** is anything renderable: an element, a string, a number, null, false, or an array of these.
Category
React
Hooks, Fiber, concurrent rendering, hydration, and performance.
218 questions
JSX is syntactic sugar over React.createElement(type, props, ...children). The compiler (Babel/SWC/esbuild) transforms <div className='x'>hi</div> into createElement('div', { className: 'x' }, 'hi'), which returns a plain object describing the element. React's reconciler walks this tree, diffs against the previous tree, and applies minimal DOM mutations. JSX is not HTML and not a template engine — it's JavaScript expressions.
React.createElement(type, props, ...children) creates a brand-new element from scratch — JSX compiles to it. React.cloneElement(element, props, ...children) copies an EXISTING element, shallow-merging new props over its original ones. cloneElement is for injecting props into children you received.
Fragments let a component return multiple elements without an extra wrapper DOM node. `<>...</>` (shorthand) or `<Fragment>...</Fragment>` (explicit, accepts a `key` prop). Use them to avoid unnecessary divs that break CSS Grid/Flexbox layouts, to keep tables valid, and to return sibling elements from map callbacks.
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.
Strict Mode is a dev-only React wrapper that surfaces unsafe patterns: double-invokes components/effects/state initializers, warns on deprecated APIs (legacy refs, findDOMNode), forbids side-effects in render. The double-mount in React 18 specifically reveals missing useEffect cleanups. Production is unchanged. Always wrap the app in <StrictMode> during development.
StrictMode intentionally double-invokes components, effects, and reducers in development to surface impure renders and effect cleanup bugs that would break under concurrent rendering.
`ReactDOM.render(<App/>, root)` is the legacy synchronous API (React 16/17). `createRoot(root).render(<App/>)` is the React 18 API that enables concurrent features: automatic batching everywhere, useTransition, useDeferredValue, streaming SSR, Suspense for data. Functionally similar for simple apps but createRoot unlocks the new scheduler. ReactDOM.render still works in 18 but logs a deprecation warning.
React 18 added concurrent rendering (interruptible renders), automatic batching everywhere (not just in event handlers), Suspense for data fetching, useTransition / useDeferredValue, useId, the new createRoot API, server components (experimental), and stricter StrictMode that double-mounts effects in dev. React 16 introduced Fiber, hooks (16.8), and error boundaries but rendering was fully synchronous.
Smaller runtime (~45 KB gzipped vs Angular's larger framework), simpler change detection (re-render on state change vs zone.js patching async APIs), virtual DOM diffing instead of dirty-checking template bindings, hooks-based composition, tree-shakeable, and a leaner mental model that's faster to optimize. Caveat: well-built Angular apps can be very fast; framework choice rarely dominates real-world perf.
Reasons: smaller bundle, larger ecosystem/hiring pool, simpler mental model (no DI, no decorators, no NgModules), faster iteration. Challenges: re-implementing two-way binding with controlled inputs, replacing RxJS with React Query/Zustand, migrating dependency injection patterns, rebuilding form validation, and the team learning JSX + the 'thinking in React' model.
React is a view library, not a framework — no built-in routing, data fetching, forms, or state management. The team picks N libraries; mismatches and version drift accumulate. Out-of-box bundle is small but unguided architecture leads to bloat. No conventions for folder structure, code splitting, SSR — different shops solve it differently. Performance is up to the developer. Hooks rules + closures are easy to get wrong. RSC is still maturing.
Functional components are plain functions that return JSX and use hooks for state/effects/refs — the modern default. Class components extend Component, use this.state / setState, and have lifecycle methods. Functional components are smaller, easier to test, free of 'this' binding bugs, and the only path to new React features (concurrent rendering, Server Components). Use class components only for error boundaries.
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).
Class components use ES6 classes, this, lifecycle methods, and this.state/setState. Function components are plain functions using Hooks for state and effects. Functions + Hooks are the modern standard — less boilerplate, better logic reuse, no `this` confusion. Both can do the same things.
Class lifecycles split a single concern across multiple methods (`componentDidMount` + `componentDidUpdate` + `componentWillUnmount`). `useEffect` lets you co-locate the whole effect — setup + cleanup — and run it whenever its dependencies change. Effects are about synchronization, not lifecycle moments.
Function components don't have lifecycle methods — they re-run top-to-bottom on every render. Hooks emulate the lifecycle: useState/useRef = instance fields; useEffect with [] = componentDidMount + componentWillUnmount; useEffect with deps = componentDidUpdate; useLayoutEffect = synchronous post-commit work; the function body itself = render. Reasoning shifts from 'phases' to 'this state at this moment'.
Props are inputs passed into a component by its parent — read-only from the component's view. State is data the component owns and manages internally, and can change over time. Props flow down and are immutable; state is local, mutable (via setState), and triggers re-renders.
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.
setState schedules a re-render; React reconciles the virtual DOM, diffs vs the previous render, and commits changes to the real DOM. Updates batch automatically (React 18+) across sync and async code. A component re-renders when its state, props, or subscribed context changes; parent re-render cascades to children unless React.memo + stable props short-circuit it. Updates from outside React (Redux, Zustand) need useSyncExternalStore for tearing-free concurrent rendering.
React detects changes by reference (Object.is) — mutating state in place keeps the same reference, so React doesn't know it changed and skips the re-render. Mutation also breaks memo/PureComponent, corrupts previous-state snapshots, and makes time-travel/debugging unreliable. Always create a new object/array.
Always produce new references for changed branches: `{...state, field: newValue}`, `state.map(...)`, `state.filter(...)`. For nested updates, replace the whole path (`{...state, a: {...state.a, b: 1}}`). Use Immer (`produce`) for ergonomic mutable-syntax-but-immutable-output, or RTK's createSlice which bakes it in.
State scheduling is how React decides WHEN to actually apply a state update and re-render. React 18 introduced automatic batching (multiple setState calls in a single tick collapse to one render) and priority-based scheduling: urgent updates (clicks, input) run synchronously; non-urgent updates wrapped in startTransition can be deferred or interrupted by higher-priority work.
Put state as close as possible to where it's used. The wrong default — 'lift everything to App' — re-renders the whole tree for any change and obscures ownership. Colocation: input state in the input, modal-open state in the modal trigger, filter state in the filter panel. Lift only when multiple siblings need it; even then, lift to the lowest common ancestor.
Passing props through multiple intermediate components that don't use them, just to reach a deep descendant. It makes code verbose, tightly couples components, and is fragile to refactor. Fixes: Context, composition (children/slots), or a state library — pick by how the data is used.
Props — the primary, one-way mechanism: parent passes values/objects/functions as attributes, child receives them as a props object. Data flows down; for child-to-parent, pass a callback prop. For deep trees, Context avoids prop-drilling. children is a special prop for nesting.
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.
Keys give React a stable identity for each item across renders. Without them React falls back to index-based matching, which corrupts state when items are inserted, removed, or reordered. A row keyed by index would 'inherit' the input value of whatever item used to be in that position. Use a stable id from your data; avoid array index when the list mutates.
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.
Keys give list items a stable identity so React's reconciler can match elements across renders. Good keys = correct minimal updates. Bad keys (index, or random each render) cause wrong state/DOM reuse, lost focus and input values, broken animations, and unnecessary re-renders.
An index isn't tied to the item — it's tied to the position. Insert, delete, reorder, or filter the list and the same index now points to different data, so React reuses the wrong DOM node and component state: wrong input values, misplaced focus, stale state, broken animations. Use a stable id.
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`.
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.
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.
Hooks are functions starting with `use` that let function components opt into React features (state, effects, context, refs). A custom hook is just a function that calls other hooks — naming convention `useXxx` enables the linter to enforce the rules of hooks. Extract reusable stateful logic by composing built-in hooks into a function.
The five hooks that cover 95% of component code: useState (local reactive state), useEffect (subscriptions/side-effects with cleanup), useRef (mutable values that survive re-renders + DOM access), useMemo (cache an expensive value across renders), useCallback (cache a function identity for memoized children).
useState fits 1–3 independent values updated with simple setters. useReducer fits state with multiple related fields or complex transitions — pull the update logic into a pure reducer (state, action) → state. Reducers are easier to test, easier to log, and naturally handle multi-step state machines. Reach for useReducer when setState calls start to multiply or several pieces of state must change together.
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.
useEffect runs AFTER React commits to the DOM. The setup function runs when deps change (or on mount if deps are empty). The cleanup returned from setup runs BEFORE the next setup AND on unmount. Missing deps cause stale closures; mutable deps cause infinite loops; effects that mutate refs don't trigger re-renders.
useEffect runs AFTER React commits to the DOM. Setup function runs when deps change or on mount; cleanup runs before next setup AND on unmount. Phase: render → commit → useLayoutEffect (sync) → browser paint → useEffect (async). StrictMode dev double-invokes effects to surface missing cleanups. Common bugs: stale closures (use functional updaters), infinite loops (use derived render), missing cleanups (return cleanup function).
useEffect runs after render, making it the classic place to fetch data on mount or when dependencies change. You manage loading/error/data state, clean up to avoid race conditions and setState-after-unmount, and re-fetch when deps change. But for real apps, prefer React Query/SWR or framework data loading.
Fetch inside useEffect with the right dependency array; in the cleanup function, abort the request (AbortController) or set an `ignore` flag so a stale/late response can't update an unmounted or superseded component. Handle loading/error states; consider a query library for real apps.
The dependency array tells React WHEN to re-run the effect — it compares deps between renders and re-runs only if one changed. No array = every render; []= once on mount; [a,b] = when a or b changes. It also defines which values the effect 'sees'; omitting deps causes stale closures or infinite loops.
Common causes: missing dependency (stale closure, value never updates), wrong dep array shape ([] when should be [x]), running twice in dev (Strict Mode, intentional), effect setting state that triggers the same effect (infinite loop), depending on object/function literal that's new every render (always re-fires), forgetting cleanup (subscriptions accumulate), async work without abort (race conditions). Use the react-hooks/exhaustive-deps lint rule + React DevTools Profiler.
It doesn't — circular useEffect dependencies cause infinite re-renders. Typical bug: effect writes to state that's in its own dep array, triggering itself. Fixes: (1) use functional updaters `setX(prev => ...)` to read latest state without depending on it, (2) move the derived value into useMemo computed in render, (3) use refs for values that should not trigger re-runs, (4) reset state via `key` prop instead of effect-driven sync.
Don't use useEffect for: deriving state (compute during render); transforming data for rendering (compute or useMemo); responding to user events (do it in the handler); resetting state on prop change (use a `key`); chaining state updates (combine into one). Effects are for *synchronizing with external systems* — DOM, subscriptions, APIs — not for in-app data flow.
`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).
Side effects (DOM mutation, network, subscriptions, timers, logging) live in useEffect — runs after commit, returns cleanup. Pure render = no side effects. Manage by: (1) keeping effects small and focused, (2) cleaning up properly, (3) avoiding state-from-state in effects (use derived render or key reset), (4) reaching for React Query for fetches, (5) using refs for non-reactive values. StrictMode dev double-mount surfaces missing cleanups.
`useRef` stores a mutable value that persists across renders without triggering re-renders. Two main uses: (1) hold a reference to a DOM node (`ref={ref}`) for measurement/focus/imperative ops, (2) store any mutable value (timer ids, latest values, previous props) that shouldn't cause re-renders. `.current` is the only field; updating it is intentional, not a state change.
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.
`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.
`forwardRef` forwards a ref through a component so parents can access the underlying DOM node or imperative API. `useImperativeHandle` customizes what the ref exposes — handy when you need to expose specific methods (`focus`, `scrollIntoView`, `open()`) without leaking the DOM node. In React 19, regular function components accept ref as a prop, removing the need for forwardRef.
Used with forwardRef, useImperativeHandle(ref, createHandle, deps) lets a child customize the value the parent's ref points to — exposing a curated API ({ focus, reset }) instead of the raw DOM node. It runs during commit. It's an escape hatch — prefer declarative props when possible.
Prefer declarative props (controlled value + event callbacks) over imperative APIs. When imperative access is genuinely needed (focus, scroll, play), expose a minimal, intentional handle via forwardRef + useImperativeHandle. Compound components for structural APIs.
`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.
React.memo: HOC that prevents a component from re-rendering when its props are shallow-equal to the previous render. useMemo: caches a COMPUTED VALUE across renders, recomputes when deps change. useCallback: caches a FUNCTION reference across renders, returns same function when deps unchanged. React.memo is about whether a child renders; useMemo/useCallback are about whether passed values change reference. Use together: memoized child + stable callback/value props.
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.
React re-renders on state change, parent re-render, context change, or external store update. Tools to prevent unnecessary renders: `React.memo(Component)` skips re-render when props are referentially equal; `useMemo(fn, deps)` caches a computed value across renders; `useCallback(fn, deps)` caches a function reference so memoized children don't bail. Combined: memoize the child, pass useCallback handlers, useMemo objects. Profile first — most uses are unnecessary.
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.
Memoization (memo/useMemo/useCallback) helps when it prevents an expensive computation or a costly subtree re-render, AND the deps are actually stable. It hurts when the work is cheap, deps change every render, or you wrap everything reflexively — then you pay comparison + memory cost for nothing.
Memoize a component when it re-renders often (parent re-renders frequently), its props are stable or can be made stable, and its render is non-trivial. Don't memoize cheap components, ones whose props change every render anyway, or as a blanket policy. Always profile first.
Shallow comparison checks reference equality for objects/arrays and value equality for primitives — what React.memo and PureComponent do by default. Deep comparison walks every nested key recursively. Shallow is cheap (O(top-level props)) but misses changes inside nested objects; deep catches everything but is O(size of tree) and can be slower than re-rendering. Use shallow + immutability discipline; reach for deep only with cause.
useCallback memoizes a function so its reference is stable across renders — unless a dependency changes. Closures inside it capture values from the render where it was created; stale deps mean stale values. The function identity matters for child memoization and effect deps.
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.
`useCallback(() => setCount(count + 1), [])` captures `count` from the render the callback was created (closure). Empty deps → it never updates → always uses the initial `count`. Fix: include `count` in deps (callback identity changes), or use the functional setter `setCount(c => c + 1)` (doesn't read outer `count`), or a ref for 'latest'.
With empty deps, the callback closes over the state value from its first render — forever stale. setCount(c => c + 1) reads the latest state from React's updater queue instead of the captured variable, so the callback stays correct without listing state as a dependency.
A custom hook is a function named `useXxx` that calls other hooks. Extract reusable stateful logic by composing built-ins: useState, useEffect, useRef, etc. Parameterize inputs, return a stable shape (`{ data, loading, error }` or `[value, setter]`), use useCallback for returned functions, document the contract. Examples: useDebounced, useFetch, useLocalStorage, useMediaQuery.
Design custom hooks like small libraries: parameterize all inputs, return a stable shape ({ data, loading, error, refetch }), keep side-effects (subscriptions, listeners) inside with cleanup, avoid hard-coding UI concerns or context dependencies, document the contract, and write tests with @testing-library/react's renderHook.
Extract hooks where logic is duplicated, stateful, or involves effects/subscriptions: data fetching, form handling, debounced values, media queries, local storage sync, intersection observers, pagination, and event listeners. Extract for reuse or clarity — not prematurely.
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.
`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.
Keep the input controlled and responsive; debounce the *derived effect* (API call / expensive filter), not the keystrokes. In React: a debounced value via useEffect + timeout, or a stable debounced callback via useMemo/useRef — never re-create the debounced fn each render.
`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.
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.
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.
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.
A controlled component's value lives in React state and is driven by props (value + onChange) — React is the single source of truth. An uncontrolled component keeps its own state in the DOM, read via a ref when needed (defaultValue). Controlled = predictable; uncontrolled = less code, fewer re-renders.
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.
Validate at the right moments (on blur / on submit, then on change once a field is touched/errored), show clear inline messages near the field, never block typing, surface a submit-time summary, mirror server validation, and make errors accessible (aria-invalid, aria-describedby, focus management).
Drive the form from a schema/config, not hardcoded JSX. Compute visible fields from current values, validate conditionally (only visible fields), clear or preserve hidden-field values intentionally, and use a form library (React Hook Form) for performance and field arrays.
Use a form library (React Hook Form) for state, validation, and field arrays rather than hand-rolling. Validate with a schema (Zod/Yup). Track per-field touched/dirty/error; validate on blur + submit. For dynamic inputs use field arrays with stable ids. Always re-validate on the server.
Store dynamic fields as an array of objects, each with a STABLE unique id (not the index). Add = append, remove = filter by id, update = map by id. The id is the React key and the lookup handle — using the index breaks values/focus on removal. A form library's useFieldArray does exactly this.
Lift the whole multi-step form's state to a parent (or context/reducer) above the step components, not inside each step. Each step reads/writes its slice; steps unmount without losing data. Persist to storage for refresh-safety, validate per step, and submit the aggregated object at the end.
Track currentStep in state; gate 'Next' on validating the current step's slice. Keep per-step errors in shared state, allow free Back navigation, mark steps visited/valid, sync step to the URL for deep-linking and refresh, and block final submit until all steps are valid.
A Higher-Order Component is a function that takes a component and returns a new component with extra behavior — `withAuth(Component)`, `connect(mapStateToProps)(Component)` (Redux). Used for cross-cutting concerns: auth, logging, theming, data fetching. Largely replaced by custom hooks (cleaner composition, no wrapper hell). Still useful for: passing refs through, wrapping all instances uniformly, adapting class components.
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).
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).
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.
Redux is a predictable global state container based on a single store, read-only state, and pure reducers updating state via dispatched actions. Modern Redux uses Redux Toolkit: createSlice generates the reducer + action creators with Immer-powered 'mutating' syntax.
createContext returns a { Provider, Consumer }. The Provider wraps a subtree with a value prop; descendants read via useContext(MyContext). Internally React walks up the fiber tree on read to find the nearest Provider; subscribers are tracked so they re-render when value changes. Pitfalls: every value change re-renders every consumer (split contexts), inline object values cause unnecessary updates (memoize).
createContext → a Provider component holding state (useState/useReducer) → consumers read via useContext. Key pitfalls: every consumer re-renders when the value changes, the value object must be stable, and unrelated state should be split into separate contexts.
Context API can act as lightweight global state for rarely-updated values (auth, theme, locale). Combine with useReducer for action-based updates. Split contexts by update frequency to limit re-renders. Memoize the Provider value. NOT a substitute for Redux/Zustand when state updates often or many components subscribe — Context re-renders every consumer on every value change.
Lift state up to the nearest common parent and pass via props. For widely-shared state across many siblings: Context API. For frequent updates with selectors: Zustand / Jotai. For server state shared between components: React Query (same cache key). For URL-driven sharing: search params via useSearchParams. For cross-tab: localStorage events or BroadcastChannel.
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.
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.
Yes — frame it around when and why: Zustand for lightweight global client state with selector-based subscriptions and minimal boilerplate; Redux Toolkit for large apps needing structure/middleware/devtools; Jotai for atomic state; and React Query for server state (a different problem).
Redux: a single store, state changed only via dispatched actions run through pure reducers, predictable and debuggable. Redux Toolkit (RTK) is the modern standard — slices, Immer, built-in thunks, less boilerplate. Know when you actually need it vs Context/Zustand/server-state libs.
Redux is the original predictable state container — verbose: hand-write action types, action creators, reducers, immutable updates. Redux Toolkit (RTK) is the official, batteries-included wrapper: `createSlice` autogenerates actions+reducers, Immer lets you 'mutate' state in reducers, `configureStore` adds DevTools + thunk by default, RTK Query handles server state. Use RTK — plain Redux is legacy.
RTK collapses Redux boilerplate: `createSlice` (reducers + actions in one place with Immer), `configureStore` (middleware + devtools wired), `createAsyncThunk` (async lifecycle), `createEntityAdapter` (normalized CRUD), RTK Query (React-Query-like data layer over Redux). Reduces boilerplate ~70% vs hand-rolled Redux.
In React you don't call store.subscribe directly — useSelector subscribes a component to the slice it reads and re-renders it on change. Under the hood the <Provider> and useSelector use store.subscribe. Vanilla Redux exposes store.subscribe(listener) returning an unsubscribe function.
Redux thunk = action creators return a function (thunk) instead of an action object. The function gets (dispatch, getState), does async work (fetch), dispatches start/success/failure actions. Caching is manual: check getState() for existing data, skip the call if fresh. No built-in dedup, TTL, or invalidation. RTK Query is the modern replacement — it gives you the cache layer thunks don't have.
Context: built-in, no extra deps, but every consumer re-renders on any value change — best for low-frequency cross-tree config (theme, auth user). Redux/RTK: predictable + devtools + middleware ecosystem, opinionated boilerplate, best for complex client domains. Zustand: minimal API, selector-based subscriptions, ~1kb, best default for small/medium apps. Server state? None of these — use React Query.
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.
Often yes, but not always. For most apps: useState + Context + React Query covers 90% of what people used Redux for (server state goes to React Query, local to useState, cross-tree config to Context). What Redux still offers: time-travel devtools, middleware ecosystem (logger, sagas, listeners), strict action-based mutation pattern useful for large teams. For complex client domains with many cross-feature interactions, RTK still earns its place.
Default to local state and lift only when sharing is needed. Distinguish server state (cache it with React Query/SWR) from client state. Use Context for low-frequency global values and a store (Zustand/Redux) for complex, high-frequency global state.
Separate concerns. Server state (API responses) → React Query / RTKQ / SWR (caching, dedup, invalidation). Client global state (theme, auth, feature flags) → Context for rarely-changing values, Zustand/Jotai for frequently-changing. URL state → router params. Local state → useState. Don't put everything in one big global store; split by domain. Avoid Context for high-frequency updates — every consumer re-renders. Use selectors / atomization for granular subscription.
Categorize state first: server (React Query), local UI (useState), cross-tree config (Context), cross-tree app state (Zustand/Redux with selectors), URL state (router), form state (RHF). Colocate where possible. Selectors > raw access. Feature-scoped slices with ownership in multi-team apps. Don't mirror server state in a client store. Avoid Context for hot state.
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.
Treat each concern as an independent slice with its own data source and hook. Charts: useChartData (React Query, polling or WebSocket). Notifications: useNotifications (subscribed via Context or external store). Data fetches: useQuery per resource. Compose at the page level. Context only for cross-cutting state (theme, auth). External libs (Zustand) when state must update at high frequency or across many consumers without re-render storms.
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.
Nest boundaries: a global root boundary catches anything that escapes, page-level boundaries isolate each route so one page's crash doesn't blank the app, and optional widget-level boundaries contain risky sections. Inner boundaries catch first; outer is the safety net.
Wrap each route in its own Suspense boundary (loading fallback) and error boundary (error fallback). Use route-level skeletons matching the page layout, and a shared layout that stays mounted while the route content suspends.
Match the fallback to the error: 404 = friendly 'not found' with search + popular links; 500 = apologetic error with retry and a status link; network = inline retry with cached content if available; auth (401/403) = redirect to login / show 'no access'; chunk-load failure = soft reload prompt. Always: clear copy, a next action, no stack traces, log to monitoring.
Layer error boundaries: a top-level boundary as the last resort, per-route boundaries for isolation, and granular boundaries around risky widgets. Pair with a window error/unhandledrejection handler, a logging service, user-friendly fallbacks with retry, and recovery on navigation.
Classic 'parent removes an item, child rendering that item crashes during the same render' bug. Fixes (best to worst): guard with conditional rendering in the parent (don't render the child if the item is gone); read the item by id in the child and bail to null if undefined; use a key that changes on removal so the child unmounts cleanly; move the read to a selector that returns undefined safely.
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.
React Router is the de-facto SPA routing library. It maps URLs to component trees, handles navigation without full page reloads, and provides hooks (`useNavigate`, `useParams`, `useLocation`) for programmatic access. Dynamic routing: `<Route path='/users/:id' element={<User/>} />` extracts `id` via `useParams()`. v6/7 adds nested routes, loaders (data fetching co-located with route), and form actions.
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.
Hold auth state in Context (or Redux), wrap private routes in a guard component that checks auth status, redirect unauthenticated users to login (preserving the intended destination), handle the loading state during the auth check, and remember client-side guards are UX — the server enforces real access.
Layered: server enforces (authoritative — API endpoints check role on every request); router-level guard component on the client checks role and redirects/denies; conditionally render UI elements (hide admin buttons for non-admins). Never trust the client — the client guard is UX only. Bake role into the auth token; refresh it when roles change.
A ProtectedRoute wrapper checks auth state; if unauthenticated, redirect to /login (preserving the intended URL for post-login return). Handle the loading state while auth resolves to avoid a flash. But remember: route guards are UX — the server must still authorize every request.
Extract the shared logic into a reusable custom hook or a query library so each component/route doesn't re-implement it. Centralize cross-cutting state (auth, theme) in context/store, code-split per route, and use a layout/loader pattern so routes share fetching, loading, and error handling consistently.
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)`).
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.
Run async in useEffect with AbortController for cancellation, or use React Query/SWR for caching, retries, and dedup. Inside event handlers, await freely but guard against unmounts. For state updates from async, use functional updaters to avoid stale closures. React 18 batches setState across promises automatically. For race conditions (rapid inputs), cancel previous in-flight requests or track a request id and discard outdated responses.
Six common interception points: (1) axios interceptors for auth headers, retries, logging; (2) a wrapped fetch helper used app-wide; (3) React Query's onError/fetcher global hooks; (4) a Service Worker for network-level intercept (offline caching); (5) MSW for tests/dev mocking; (6) browser DevTools Network throttling/blocking for ad-hoc cases.
No — React itself doesn't make API calls. The developer does, via fetch/axios/React Query. To parallelize, the developer uses Promise.all (fails fast) or Promise.allSettled (resolves to per-promise results regardless of failures). React Query handles parallelism via independent useQuery hooks that fire concurrently. Suspense + use() enables waterfall avoidance via parallel data dependencies.
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.
Model status as an enum (idle/loading/success/error/empty), not a boolean. Use skeleton screens over spinners for content, match the skeleton to the real layout to avoid shift, debounce indicators for fast loads, and use Suspense for code/data loading. Always handle empty and error too.
Track page/cursor, page size, items, total/hasMore, plus discrete status (idle/loading/success/error). Distinguish initial load (skeleton) from page change (spinner) from background refetch. Offset vs cursor pagination. A query library handles most of this; know what it's doing.
Decide where filtering lives: server-side for large data (filters become query params, debounced) or client-side for small data (memoized derive from a source list — don't store filtered results as separate state). Sync filters to the URL, handle loading/empty/error per filter change, and cancel stale requests.
Split into layers: data fetching/state in hooks or container components, pure presentational components that render data via props, and UI controls that raise events upward. Keep rendering components dumb and testable; keep data logic reusable and isolated.
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).
`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.
`useActionState` (React 19) wraps an async server/client action. It returns `[state, dispatch, isPending]` — `state` is the action's last return value, `dispatch` invokes the action (queues across calls), `isPending` reflects in-flight status. Designed for forms: `<form action={dispatch}>` works with progressive enhancement. Replaces a lot of useState + useTransition boilerplate.
Optimistic update on click: instantly increment the cart count in the React Query cache, then fire the mutation. On error, rollback. Use `useMutation` with `onMutate` (snapshot + optimistic write), `onError` (restore snapshot), `onSettled` (invalidate to refetch source of truth). For multi-user race conditions, server returns the canonical state and we reconcile. Show a 'syncing' indicator if the mutation is in flight.
WebSocket (or SSE for one-way) → push updates into a normalized React Query cache or Zustand store. Dedup by id. On reconnect, fetch missed events since last timestamp. Memoize selectors; virtualize the list view. Backoff + jitter on reconnect. Use AbortController to cancel stale fetches. Optimistic UI for sends; reconcile when server confirms.
Batch incoming messages instead of one setState per message; React 18 auto-batches but a buffer + flush on rAF/interval helps for bursts. Virtualize the message list. Keep updates off the critical path: process/parse in chunks or a Web Worker, use stable keys, and memoize message rows.
Slice state by widget, use selectors with referential stability, isolate live-update components behind their own subscriptions, and memoize where measurement justifies it.
Lazy loading defers downloading/executing a component until it's actually needed. `React.lazy(() => import('./Heavy'))` returns a component that fetches its chunk on first render, wrapped in `<Suspense fallback={...}>`. Improves initial bundle size, time to interactive, and LCP. Best for routes, heavy widgets (charts, editors), and modals. Pair with preload-on-intent (hover) to hide network latency.
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.
`React.lazy(() => import('./X'))` returns a component that resolves on first render via dynamic import — the bundler emits a separate chunk. Wrap it in `<Suspense fallback={<Spinner/>}>` to show UI while the chunk downloads. Best for route-level code splitting and heavy components (charts, editors, modals) that aren't on the critical path.
`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.
A component re-renders when: (1) its state changes via setState, (2) its parent re-renders, (3) a context it consumes changes, (4) a hook it uses signals a change. Re-rendering is React's default — it does NOT mean a DOM update; React diffs the output and only commits actual changes. To skip a re-render, use React.memo with stable props, split contexts, or move state down.
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.
Re-renders happen when state, props, or context changes. Unnecessary ones: parent re-renders cascade to children even when their props are identical; new object/array/function literals on every render bust React.memo; context value object recreated each render re-renders every consumer. Fix selectively: lift state down, split context, memoize stable references with useCallback/useMemo, use React.memo on expensive subtrees, profile before optimizing. Don't memoize everything — measure first.
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.
Measure first with the React DevTools Profiler and the 'highlight updates' / why-did-you-render tooling. Identify the cause (unstable props, context, parent re-render), then fix with memoization, state colocation, splitting context, or stabilizing references — not by sprinkling React.memo everywhere.
Classify state and place it well (server-cache lib for server state, colocate local state, selector stores for global), keep state low and narrow, split contexts, stabilize references, memoize selectively, and measure with the Profiler. Structure beats sprinkling memo everywhere.
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.
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.
Categories: initial load (code splitting, RSC, image pipeline, critical CSS), runtime renders (memo + stable refs, split context, virtualize, defer non-urgent updates), data layer (React Query for caching/dedup, optimistic UI), bundle hygiene (analyzer, tree shaking, dynamic imports), observability (web-vitals RUM, Sentry). Profile before optimizing. Structural fixes beat micro-memo.
Layered: (1) measure with Profiler + Lighthouse; (2) ship less JS — code split per route, lazy-load heavy components, prune deps; (3) render less — stable keys, React.memo on hot rows, useMemo/useCallback for stable identities, split contexts; (4) defer non-urgent work with startTransition / useDeferredValue; (5) virtualize long lists; (6) optimize images/fonts; (7) cache server state with React Query. Profile FIRST.
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.
1) Reproduce with throttling. 2) React DevTools Profiler — record interaction, look at flame graph (slow renders), 'Why did this render', highlight updates for visual fan-out. 3) Chrome Performance panel for main-thread/long-tasks/layout/paint. 4) Identify category: render fan-out, expensive single render, layout thrash, or non-React JS. 5) Targeted fix. 6) Measure delta. Always profile production builds for accurate numbers.
Audit: bundle analyzer (heavy deps), Lighthouse (render-blocking, LCP image), web-vitals RUM. Fixes: replace heavy deps (moment → date-fns), code split per route + lazy modals/charts, RSC for non-interactive parts, AVIF/WebP + responsive srcset for LCP image, preconnect to CDN, critical CSS inline, self-host fonts, eliminate render-blocking JS. Each lever measured for delta.
Subscribe to the window resize event in a useEffect, store the dimension in state so a change triggers a re-render, and clean up the listener on unmount. Throttle/debounce the handler for performance. Extract it into a reusable useWindowSize hook; consider ResizeObserver for element-level sizing.
Chrome DevTools Memory tab: take a heap snapshot, navigate away from the suspect feature, force GC, take another snapshot. Compare. Filter by 'Detached HTMLElement' or by component class names. Performance Monitor shows growing heap or DOM node count over time. Common culprits: uncleaned event listeners, timers, subscriptions, refs holding unmounted DOM, closures capturing large state. StrictMode dev double-mount surfaces missing cleanups.
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.
1) Reproduce — repeatedly trigger the suspected leaky path. 2) DevTools Memory panel — take heap snapshot, repeat the leak path, take another, compare; look at retained size + 'Detached HTMLElement' rows. 3) Identify the retainer — usually a missing listener removal, interval not cleared, or closure over big data. 4) Fix with cleanup in useEffect return / AbortController / bounded cache. 5) Verify heap returns to baseline.
React re-renders by calling the component function again, building a new VDOM, diffing, and committing minimal DOM changes. Memory: each fiber persists across renders (state, refs, effect cleanups). Old VDOM nodes are discarded for GC. Memory leaks come from uncleaned subscriptions, timers, detached DOM references in refs, and closures that capture large objects. Effect cleanup is the primary defense.
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.
Virtualize: render only the visible rows (react-window, TanStack Virtual). Memoize each row (React.memo) with stable callbacks (useCallback). Use stable ids for keys. Move filter/sort into useMemo or a Web Worker. Server pagination for very large data. For tables: sticky header outside scroll container, sticky columns via position sticky. Profile to verify 60fps scroll.
Profile first. Then: split context so updates fan out narrowly, memoize expensive subtrees with stable prop refs, virtualize lists, lift hot state out of Context to a selector store, use `useDeferredValue` / `startTransition` for non-urgent updates, RSC for non-interactive parts, code split routes. Avoid the 'memoize everything' anti-pattern — most renders are cheap.
Layer fixes: (1) virtualize the rendered list (TanStack Virtual / react-window) so DOM stays constant; (2) paginate or cursor-fetch the data so memory stays bounded; (3) memoize row components + stable item props; (4) avoid re-creating object/function literals in JSX; (5) useTransition for filter/sort updates so input stays snappy; (6) move heavy compute (parsing, sorting) off the main thread with web workers. Measure with Profiler before each change.
Don't render 100k items. Virtualize so only the visible ~30 are mounted (react-window, TanStack Virtual). Stable ids as keys. Memoize the row component. Move filter/sort into useMemo or a Web Worker. Paginate or chunk loading from the API. For tables: pin a header, scroll only the body. For search: index once, filter the index, not the full list.
Measure rows as they render, cache their heights, and maintain a running offset index (prefix sums) to map scroll position to row index. Use estimated heights for unmeasured rows and a ResizeObserver to catch changes after mount.
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.
Combine TanStack Query's useInfiniteQuery (paginated fetching, cursors, dedup, retry) with IntersectionObserver on a sentinel near the list bottom. Show loading sentinel, handle error with retry button, handle empty + end-of-list. For massive datasets pair with virtualization (TanStack Virtual). Make sure cleanup observers on unmount, AbortController on stale fetches, and the IO doesn't double-fire when pages arrive.
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.
Default to a battle-tested library (react-window, @tanstack/react-virtual, virtua) — they handle the gnarly edge cases. Build your own only when you have unusual requirements the libraries can't meet, and understand the core algorithm either way.
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 is the process where the client React runtime takes over server-rendered HTML: it walks the existing DOM, attaches event listeners, and reconciles state. Server sends HTML for fast first paint; client `hydrateRoot()` makes it interactive. Mismatches between server and client markup throw hydration errors. Modern variants: partial hydration, streaming SSR, React Server Components.
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.
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.
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.
Reconciliation is React's algorithm for diffing two element trees and computing the minimum DOM mutations to transform the previous tree into the new one. Heuristics: (1) different element types remount the whole subtree; (2) same type diffs attributes/children; (3) keys identify list items across renders. O(n) where n = tree size, vs naive O(n³) for general tree diff. Fiber makes the walk interruptible.
Fiber is React 16's rewrite of the reconciler that splits rendering work into small units of work (fibers), each interruptible. The scheduler can pause a render, yield to the browser for high-priority work (input, animation), then resume — keeping the main thread responsive. Enables concurrent features (useTransition, Suspense, time-slicing). Pre-Fiber React (Stack reconciler) was synchronous and blocked the main thread on big trees.
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.
Fiber is a unit of work — each component instance is a Fiber node with state, refs, hooks list, and tree pointers. Reconciliation walks the fiber tree, diffs against previous, marks changes. Work is broken into chunks the scheduler can pause/resume — enabling concurrent rendering (interruption, prioritization). Replaces pre-16's synchronous stack-based reconciler.
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.
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).
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.
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.
CSS custom properties for theme tokens, toggled via a data-attribute/class on <html>. Theme state in Context, persisted to localStorage, defaulting to prefers-color-scheme. Critical detail: apply the theme before first paint (inline script) to avoid a flash of the wrong theme (FOUC).
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.
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.
Global theming as the foundation (design tokens via CSS custom properties on the root), with scoped overrides where genuinely needed — a subtree can redefine tokens locally. Per-component theming should be the exception, layered on the global system, not a parallel system.
Persist locally in localStorage for instant application (read before first paint to avoid FOUC), and sync to the user's server profile so it follows them across devices. Respect prefers-color-scheme as the default, and apply via a data-attribute/class on the root.
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.
Open-ended 'build a UI component' prompt. Approach: clarify the spec (what does it do, what props, what variants), sketch the API first, build the simplest working version, then layer accessibility, keyboard support, controlled/uncontrolled modes, and edge cases. Examples: dropdown, modal, tabs, tooltip. Lead with the API; the implementation follows.
Identify what's variable (data, behavior, presentation) vs fixed (the pattern). Lift variable bits to props: typed data, callbacks for behavior, slot props or `children` / `renderItem` for presentation. Keep the API minimal — fewer props that compose well beat many flags. Style via tokens or `className` passthrough. Document with stories/examples.
Organize by feature, not by file type. Separate presentational from container/logic concerns, extract reusable primitives into a shared UI layer, keep components small and single-responsibility, and colocate related files (component, styles, test, hooks).
Six approaches: (1) plain CSS / SASS files; (2) CSS Modules — scoped class names per file; (3) Tailwind / Atomic CSS — utility classes; (4) CSS-in-JS (styled-components, emotion) — JS-authored styles with dynamic props; (5) zero-runtime CSS-in-JS (vanilla-extract, Linaria, Panda); (6) framework-native (Next.js global + module CSS). Modern default: Tailwind or CSS Modules. Avoid runtime CSS-in-JS for hot paths.
Common build. Store expanded state as a Set<id> for O(1) toggle. Each row reads its expanded state and renders children when open. For 'only one open at a time' use a single id. For animation, use height: auto with transform tricks or framer-motion's AnimatePresence. Avoid storing expanded state inside the row component if you need controlled state from the parent.
Compound component pattern: <Tabs value, onValueChange><Tabs.List><Tabs.Trigger value /></Tabs.List><Tabs.Panel value /></Tabs>. Use Context to share active value between List and Panels without prop drilling. A11y: ARIA roles tablist/tab/tabpanel, aria-selected, aria-controls/labelledby, keyboard support (Left/Right/Home/End to navigate, Tab to enter panel). Support controlled + uncontrolled. For production, just use Radix or React Aria — they handle the dozen edge cases.
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.
Central queue with a max visible cap, priorities (`error > warning > info > success`), and a dedupe key to merge repeat messages. Important toasts (errors) preempt; low-priority toasts wait. Each toast tracks its own timer; pause on hover; aria-live for screen readers. Use the Sonner / Radix Toast primitives instead of rolling your own.
Keep two lists: visible (capped at N) and a queue. When a toast is dismissed, promote the next queued one. Track timers per visible toast. Optionally de-dupe and prioritize errors. The key is separating 'added' from 'shown'.
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.
Two stacks (undo and redo) holding either inverse-operation commands or state snapshots. Push to undo on each action and clear redo; undo pops to redo and vice versa. Use the Command pattern for memory efficiency; cap history size and consider patches for large state.
Classic interview build. Keys move from index → stable id. Split state by concern (input vs list). Memoize the row component with React.memo and pass stable callbacks via useCallback. Use functional updaters in setState to avoid stale closures. For very long lists, virtualize with react-window. Avoid putting all todos in a single object that gets re-cloned per keystroke.
Yes — CRA is deprecated. Modern starters use Vite (`npm create vite@latest`) or Next.js (`npx create-next-app`). For full control, install `react`, `react-dom`, a bundler (Vite/esbuild/Rspack), TypeScript, and an entry that calls `createRoot(...).render(<App/>)`. CRA is slow and unmaintained — there is no reason to start with it in 2025+.
Feature-first folders, data/logic/presentation separation, and deliberate state placement: local by default, lift when shared, server-cache library for server state, Context for low-frequency globals, a store for complex global client state, URL for shareable state.
Feature-folder structure (group by feature, not by file type), clear layers (UI primitives → domain components → pages), absolute imports, shared utilities/types in lib, server state via React Query, route-level code splitting, error boundaries per route, ESLint + Prettier + TypeScript + tests, monorepo if multiple apps share code. Keep boundaries explicit: features import from lib, not from each other. Document deviations in README.
Typed props/state catch bugs at compile time. Generics for reusable components (`<List<T>>`) and hooks (`useState<User>`). Discriminated unions for state machines (`{ status: 'loading' } | { status: 'success', data: T }`). Editor autocomplete for prop names and event types. Refactor-safe rename. API contract typing with Zod + inferred types. Best with strict mode on and exhaustive deps lint.
Use a feature-flag/experiment SDK (GrowthBook, Statsig, LaunchDarkly, Optimizely, Vercel/Posthog flags). Initialize on app start with a stable user id, then read variant via a hook: `const variant = useExperiment('checkout-cta')`. Render conditionally. Track exposure + conversion to the same analytics pipeline. SSR needs the variant resolved on the server to avoid hydration flicker. Always have a kill switch.
Top hits: server data in Redux (reinventing React Query); over-memoization; missing keys / unstable keys (causing remounts); huge bundle from named-namespace imports; CSR for content pages; missing error boundaries; useEffect dep arrays wrong (stale closures or missing deps); state too high in tree; not handling loading/error/empty states; no a11y; no error monitoring; no perf budget; lazy-loading the LCP image.
`useState(initial)` returns `[value, setter]`. Polyfill via the component's hook slot array: on first render, store initial; on subsequent renders, return whatever's in the slot. The setter schedules a re-render (with batching) and writes the new value into the slot.
`useEffect(fn, deps)` runs `fn` after commit if deps changed (or every commit if no deps); calls the previous cleanup before each re-run and on unmount. Polyfill: store `{ deps, cleanup }` in the hook slot; in a 'after-render' phase, compare deps; if changed, run cleanup then fn, save new cleanup.
useRef returns a stable mutable object { current } that persists across renders and does NOT trigger a re-render when mutated. The polyfill: use useState's lazy initializer once to create and hold a single { current } object — the trick is reusing the SAME object every render.
`useMemo(factory, deps)` runs `factory()` on first render and again only when deps change (by `Object.is`); otherwise returns the cached value. Polyfill: store `{ value, deps }` in the component's hook slot array; on render, compare deps to previous; reuse or recompute.
`useCallback(fn, deps)` returns a stable function reference until `deps` change; equivalent to `useMemo(() => fn, deps)`. Polyfill via the same hook slot machinery: store [fn, prevDeps] across renders; if deps unchanged, return the previous fn; else store and return the new one.
useReducer can be built on useState: hold state in useState, and dispatch = a stable callback (useCallback/useRef) that calls setState(prev => reducer(prev, action)). Support the lazy init (third arg). The insight: useReducer is useState with the update logic centralized in a pure reducer.
`useContext` reads the value from the nearest `<Context.Provider>` ancestor. Polyfill conceptually: each Context owns a stack of currently-rendering Provider values; useContext returns the top. In React's real implementation it's woven into fiber — but for an interview, a minimal store + Provider that pushes/pops via a render-tracking mechanism suffices.
useImperativeHandle customizes the value a parent's ref sees when used with forwardRef. A polyfill: a hook that, given a ref and a factory, assigns the factory's result to ref.current in a layout effect and cleans up on unmount — keyed on deps.
useLayoutEffect runs synchronously after DOM mutations but before the browser paints — unlike useEffect which runs after paint. A 'polyfill' isn't a userland thing (it needs the commit phase); the real answer is explaining the timing and that on the server it must fall back to useEffect to avoid warnings.