JSX is a syntax extension for JavaScript that lets you write HTML-like markup inside JS files. Babel (or the TypeScript compiler) transforms every JSX element into a `React.createElement()` call (or `_jsx()` in the new JSX transform). The result is a plain JavaScript object called a React element that describes what should appear on screen.
jsx
// JSX
const el = <h1 className="title">Hello</h1>;
// Compiled (new JSX transform)
const el = _jsx("h1", { className: "title", children: "Hello" });
The virtual DOM is an in-memory JavaScript representation of the real DOM tree. When state changes, React builds a new virtual DOM, diffs it against the previous snapshot (reconciliation), and only applies the minimal set of real DOM mutations needed. This batches and minimises expensive browser layout/paint operations. In React 18, the fiber architecture makes this process interruptible so high-priority updates can preempt low-priority ones.
A controlled input stores its value in React state and every change goes through an `onChange` handler — React is the single source of truth. An uncontrolled input stores its value in the DOM and you read it via a ref when needed. Controlled inputs give you real-time validation and easy form reset; uncontrolled inputs are simpler for non-critical fields and work well with third-party DOM libraries.
Props are read-only inputs passed from a parent component; a component cannot modify its own props. State is mutable data managed inside a component that, when changed, triggers a re-render. Props flow downward (one-way data flow); state is local unless lifted up or placed in context/store.
`useState` is a Hook that lets a function component hold reactive state. It returns a two-element tuple: the current state value and a setter function. Calling the setter schedules a re-render with the new value. You can pass an initializer function to `useState` to avoid expensive initial computations on every render.
`useEffect` runs side effects after the browser has painted. It fires after every render by default; adding a dependency array limits firing to when those values change; an empty array means "run once after mount". A cleanup function returned from the effect runs before the next effect and on unmount. In React 18 with `<StrictMode>`, effects mount, unmount, and remount once in development to detect missing cleanups.
`useEffect` runs side effects after the browser has painted. It fires after every render by default; adding a dependency array limits firing to when those values change; an empty array means "run once after mount". A cleanup function returned from the effect runs before the next effect and on unmount. In React 18 with `<StrictMode>`, effects mount, unmount, and remount once in development to detect missing cleanups.
Keys help React identify which items in a list have changed, been added, or removed so it can reuse existing DOM nodes instead of recreating them. Without stable keys React falls back to index-based matching, which causes bugs when items are reordered or removed — components can receive stale state from a previous item at that index. Keys must be stable, unique among siblings, and not array indices when the list can change.
Keys help React identify which items in a list have changed, been added, or removed so it can reuse existing DOM nodes instead of recreating them. Without stable keys React falls back to index-based matching, which causes bugs when items are reordered or removed — components can receive stale state from a previous item at that index. Keys must be stable, unique among siblings, and not array indices when the list can change.
React enforces a unidirectional data flow: data passes down from parent to child via props, and children communicate back up only by calling callback functions passed as props. This makes the data flow predictable and easy to debug — you can always trace the source of a value by following the prop chain upward.
React enforces a unidirectional data flow: data passes down from parent to child via props, and children communicate back up only by calling callback functions passed as props. This makes the data flow predictable and easy to debug — you can always trace the source of a value by following the prop chain upward.
Lifting state up means moving shared state to the closest common ancestor of two or more components that need it, then passing it down as props. You do this when sibling components need to stay in sync. Over-lifting can cause unnecessary re-renders, so only lift as high as required.
Lifting state up means moving shared state to the closest common ancestor of two or more components that need it, then passing it down as props. You do this when sibling components need to stay in sync. Over-lifting can cause unnecessary re-renders, so only lift as high as required.
`React.Fragment` (shorthand `<>…</>`) lets you group multiple elements without adding an extra DOM node. Use it when a component must return more than one root element — for example, returning a `<dt>` and `<dd>` pair inside a `<dl>`. The longhand `<React.Fragment key={k}>` is required when you need a `key` prop on each fragment in a list.
`React.Fragment` (shorthand `<>…</>`) lets you group multiple elements without adding an extra DOM node. Use it when a component must return more than one root element — for example, returning a `<dt>` and `<dd>` pair inside a `<dl>`. The longhand `<React.Fragment key={k}>` is required when you need a `key` prop on each fragment in a list.
Conditional rendering means showing different UI based on a condition. Common patterns are the ternary operator (`condition ? <A/> : <B/>`), the short-circuit `&&` operator for optional elements, and early returns inside the component function. Avoid returning `0` with `&&` — use `!!count && …` or a ternary to prevent rendering the literal zero.
Conditional rendering means showing different UI based on a condition. Common patterns are the ternary operator (`condition ? <A/> : <B/>`), the short-circuit `&&` operator for optional elements, and early returns inside the component function. Avoid returning `0` with `&&` — use `!!count && …` or a ternary to prevent rendering the literal zero.
React uses synthetic events — cross-browser wrapper objects around native DOM events. You attach handlers via camelCase props like `onClick`, `onChange`, `onSubmit`. Handlers receive the synthetic event object. Under the hood React 17+ attaches all event listeners to the root container (not `document`), which makes embedding multiple React roots safe.
React uses synthetic events — cross-browser wrapper objects around native DOM events. You attach handlers via camelCase props like `onClick`, `onChange`, `onSubmit`. Handlers receive the synthetic event object. Under the hood React 17+ attaches all event listeners to the root container (not `document`), which makes embedding multiple React roots safe.
```jsx
<button onClick={(e) => { e.preventDefault(); handleClick(); }}>
Submit
</button>
```
Functional components are plain JavaScript functions that return JSX; they use Hooks for state and lifecycle. Class components extend `React.Component`, use `this.state` and lifecycle methods like `componentDidMount`. Functional components are now the recommended approach — they are simpler, have less boilerplate, and Hooks give them full lifecycle power. Class components are still supported but receive no new features.
Functional components are plain JavaScript functions that return JSX; they use Hooks for state and lifecycle. Class components extend `React.Component`, use `this.state` and lifecycle methods like `componentDidMount`. Functional components are now the recommended approach — they are simpler, have less boilerplate, and Hooks give them full lifecycle power. Class components are still supported but receive no new features.
`React.memo` is a higher-order component that memoizes a functional component's rendered output. It does a shallow comparison of props; if props are identical to the previous render, React skips re-rendering the component and reuses the last result. It is useful for pure components that receive the same props frequently — but it adds comparison overhead, so only use it when profiling shows a benefit.
`React.memo` is a higher-order component that memoizes a functional component's rendered output. It does a shallow comparison of props; if props are identical to the previous render, React skips re-rendering the component and reuses the last result. It is useful for pure components that receive the same props frequently — but it adds comparison overhead, so only use it when profiling shows a benefit.
Prop drilling is passing props through several intermediate components that do not need the data themselves — only to get it to a deeply nested child. It creates unnecessary coupling, makes refactoring harder, and forces intermediate components to accept and forward props they have no use for. Solutions include React Context, a global state library, or component composition.
Prop drilling is passing props through several intermediate components that do not need the data themselves — only to get it to a deeply nested child. It creates unnecessary coupling, makes refactoring harder, and forces intermediate components to accept and forward props they have no use for. Solutions include React Context, a global state library, or component composition.
Context provides a way to share a value (theme, locale, auth user) across the component tree without passing props at every level. You create a context with `React.createContext`, wrap a subtree with its `Provider`, and consume the value with `useContext`. Context re-renders all consumers when the value reference changes, so memoize the value object to avoid over-rendering.
Context provides a way to share a value (theme, locale, auth user) across the component tree without passing props at every level. You create a context with `React.createContext`, wrap a subtree with its `Provider`, and consume the value with `useContext`. Context re-renders all consumers when the value reference changes, so memoize the value object to avoid over-rendering.
`createRoot` (React 18) is the new entry point that opts the app into concurrent features including automatic batching, transitions, and streaming SSR. `ReactDOM.render` (React 17 legacy) runs in legacy synchronous mode without concurrent features and was removed from the public API in React 19. `createRoot` returns a root object with `.render()` and `.unmount()` methods.
`createRoot` (React 18) is the new entry point that opts the app into concurrent features including automatic batching, transitions, and streaming SSR. `ReactDOM.render` (React 17 legacy) runs in legacy synchronous mode without concurrent features and was removed from the public API in React 19. `createRoot` returns a root object with `.render()` and `.unmount()` methods.
```jsx
// React 18
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
```
`StrictMode` is a development-only wrapper that activates additional checks and warnings. In React 18, it intentionally double-invokes component renders and effects (mount → unmount → remount) to surface bugs caused by non-idempotent render logic or missing effect cleanups. It has no effect in production. Wrap your entire app or specific subtrees to catch potential issues early.
`StrictMode` is a development-only wrapper that activates additional checks and warnings. In React 18, it intentionally double-invokes component renders and effects (mount → unmount → remount) to surface bugs caused by non-idempotent render logic or missing effect cleanups. It has no effect in production. Wrap your entire app or specific subtrees to catch potential issues early.
A portal renders children into a DOM node that lives outside the parent component's DOM hierarchy using `ReactDOM.createPortal(child, container)`. Events still bubble through the React tree (not the DOM tree), so context and event delegation work as expected. Common uses: modals, tooltips, and dropdown menus that need to escape `overflow: hidden` or stacking context constraints.
A portal renders children into a DOM node that lives outside the parent component's DOM hierarchy using `ReactDOM.createPortal(child, container)`. Events still bubble through the React tree (not the DOM tree), so context and event delegation work as expected. Common uses: modals, tooltips, and dropdown menus that need to escape `overflow: hidden` or stacking context constraints.
Refs provide a way to access a DOM node or a mutable value that persists across renders without triggering re-renders. Use `useRef` to hold the mutable object; attach to DOM elements via the `ref` prop. Use refs for managing focus, triggering animations, integrating with third-party DOM libraries, or storing interval/timer IDs. Avoid using refs to read or write data that should drive UI — use state for that.
Refs provide a way to access a DOM node or a mutable value that persists across renders without triggering re-renders. Use `useRef` to hold the mutable object; attach to DOM elements via the `ref` prop. Use refs for managing focus, triggering animations, integrating with third-party DOM libraries, or storing interval/timer IDs. Avoid using refs to read or write data that should drive UI — use state for that.
`children` is a special prop that contains whatever is nested between the opening and closing tags of a component. It can be a string, a React element, an array, or a function (render prop). You access it via `props.children` (class) or destructuring in function components. `React.Children` utilities help when you need to iterate or count children safely.
`children` is a special prop that contains whatever is nested between the opening and closing tags of a component. It can be a string, a React element, an array, or a function (render prop). You access it via `props.children` (class) or destructuring in function components. `React.Children` utilities help when you need to iterate or count children safely.
`React.cloneElement(element, extraProps, ...children)` creates a shallow copy of a React element with additional or overridden props merged in. It is typically used in compound component patterns where a parent needs to inject props (e.g., an `isActive` flag) into child elements it does not own. Context or render props are often cleaner alternatives.
`React.cloneElement(element, extraProps, ...children)` creates a shallow copy of a React element with additional or overridden props merged in. It is typically used in compound component patterns where a parent needs to inject props (e.g., an `isActive` flag) into child elements it does not own. Context or render props are often cleaner alternatives.
A render prop is a function prop that a component calls to know what to render, enabling logic reuse without wrapping the tree in extra DOM nodes. The parent component controls the data and calls `this.props.render(data)` (or `props.children(data)`) to delegate the UI. Hooks have largely replaced render props for logic reuse, but the pattern is still useful for library APIs like `react-router`'s `<Route render={…} />`.
A render prop is a function prop that a component calls to know what to render, enabling logic reuse without wrapping the tree in extra DOM nodes. The parent component controls the data and calls `this.props.render(data)` (or `props.children(data)`) to delegate the UI. Hooks have largely replaced render props for logic reuse, but the pattern is still useful for library APIs like `react-router`'s `<Route render={…} />`.
A HOC is a function that takes a component and returns a new enhanced component, following the same pattern as higher-order functions. HOCs are used for cross-cutting concerns like authentication guards, analytics logging, or data fetching wrappers. Because they add wrapper components to the tree and can obscure the component origin, Hooks are now preferred for most of these use cases.
A HOC is a function that takes a component and returns a new enhanced component, following the same pattern as higher-order functions. HOCs are used for cross-cutting concerns like authentication guards, analytics logging, or data fetching wrappers. Because they add wrapper components to the tree and can obscure the component origin, Hooks are now preferred for most of these use cases.
A component re-renders when its state changes (via setter), when its parent re-renders (even if props did not change), or when a context value it consumes changes. `React.memo`, `useMemo`, and `useCallback` can prevent re-renders in some cases, but they add overhead and should only be applied after profiling confirms a real problem.
A component re-renders when its state changes (via setter), when its parent re-renders (even if props did not change), or when a context value it consumes changes. `React.memo`, `useMemo`, and `useCallback` can prevent re-renders in some cases, but they add overhead and should only be applied after profiling confirms a real problem.
Default props provide fallback values when a prop is not passed or is `undefined`. In function components you use parameter default values: `function Button({ color = "blue" })`. In class components you set `Component.defaultProps = { … }`. The `defaultProps` static property on function components is deprecated in React 19.
Default props provide fallback values when a prop is not passed or is `undefined`. In function components you use parameter default values: `function Button({ color = "blue" })`. In class components you set `Component.defaultProps = { … }`. The `defaultProps` static property on function components is deprecated in React 19.
Class components have three phases: mounting (`constructor` → `render` → `componentDidMount`), updating (`shouldComponentUpdate` → `render` → `componentDidUpdate`), and unmounting (`componentWillUnmount`). In function components, `useEffect` covers `componentDidMount`, `componentDidUpdate`, and `componentWillUnmount` via its dependency array and cleanup return.
Class components have three phases: mounting (`constructor` → `render` → `componentDidMount`), updating (`shouldComponentUpdate` → `render` → `componentDidUpdate`), and unmounting (`componentWillUnmount`). In function components, `useEffect` covers `componentDidMount`, `componentDidUpdate`, and `componentWillUnmount` via its dependency array and cleanup return.
`React.PureComponent` is a class component base that implements `shouldComponentUpdate` with a shallow prop and state comparison. If all props and state references are equal, the update is skipped. The functional equivalent is `React.memo`. Both assume immutable data — mutating an object in place will not trigger a re-render even though the data changed.
`React.PureComponent` is a class component base that implements `shouldComponentUpdate` with a shallow prop and state comparison. If all props and state references are equal, the update is skipped. The functional equivalent is `React.memo`. Both assume immutable data — mutating an object in place will not trigger a re-render even though the data changed.
`componentDidMount` fires once after initial mount, synchronously in the commit phase but before the browser paints in some React versions. `useEffect` with an empty dependency array fires after every mount but asynchronously after the browser has painted, so it does not block the user from seeing the first frame. For DOM measurements that must happen before paint, use `useLayoutEffect` instead.
`componentDidMount` fires once after initial mount, synchronously in the commit phase but before the browser paints in some React versions. `useEffect` with an empty dependency array fires after every mount but asynchronously after the browser has painted, so it does not block the user from seeing the first frame. For DOM measurements that must happen before paint, use `useLayoutEffect` instead.
Reconciliation is the process React uses to update the DOM efficiently. When state changes, React produces a new element tree and diffs it against the previous one using a heuristic O(n) algorithm: if root element types differ, it unmounts the old tree entirely; if they are the same type, React updates the existing instance with new props; `key` props guide list reconciliation. The Fiber architecture makes this process interruptible.
Reconciliation is the process React uses to update the DOM efficiently. When state changes, React produces a new element tree and diffs it against the previous one using a heuristic O(n) algorithm: if root element types differ, it unmounts the old tree entirely; if they are the same type, React updates the existing instance with new props; `key` props guide list reconciliation. The Fiber architecture makes this process interruptible.
`useMemo` memoizes the **return value** of a computation; `useCallback` memoizes the **function itself**. `useCallback(fn, deps)` is equivalent to `useMemo(() => fn, deps)`. Use `useMemo` for expensive calculations and `useCallback` when passing callbacks to memoized child components (via `React.memo`) so that the child's props equality check passes. Both should be applied after profiling — premature memoization adds overhead and cognitive cost.
`useMemo` memoizes the **return value** of a computation; `useCallback` memoizes the **function itself**. `useCallback(fn, deps)` is equivalent to `useMemo(() => fn, deps)`. Use `useMemo` for expensive calculations and `useCallback` when passing callbacks to memoized child components (via `React.memo`) so that the child's props equality check passes. Both should be applied after profiling — premature memoization adds overhead and cognitive cost.
```jsx
const sortedList = useMemo(() => list.sort(compareFn), [list]);
const handleClick = useCallback(() => onSelect(id), [id, onSelect]);
```
`useRef` returns a mutable object whose `.current` property persists across renders without triggering re-renders when changed. Beyond DOM refs, it is used to: store the previous value of a prop or state; hold a timer or interval ID for cleanup; cache a stable reference to a callback or value that must not be a dependency in other hooks; and track whether a component is still mounted in async operations. It is essentially a mutable instance variable for function components.
`useRef` returns a mutable object whose `.current` property persists across renders without triggering re-renders when changed. Beyond DOM refs, it is used to: store the previous value of a prop or state; hold a timer or interval ID for cleanup; cache a stable reference to a callback or value that must not be a dependency in other hooks; and track whether a component is still mounted in async operations. It is essentially a mutable instance variable for function components.
A custom Hook is a function whose name starts with `use` and that calls other Hooks. It extracts stateful logic so multiple components can share it without altering their tree. The two rules of Hooks apply: only call Hooks at the top level (not inside loops, conditions, or nested functions), and only call Hooks from React functions. Custom Hooks do not share state between callers — each call gets its own isolated state.
A custom Hook is a function whose name starts with `use` and that calls other Hooks. It extracts stateful logic so multiple components can share it without altering their tree. The two rules of Hooks apply: only call Hooks at the top level (not inside loops, conditions, or nested functions), and only call Hooks from React functions. Custom Hooks do not share state between callers — each call gets its own isolated state.
```tsx
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
return width;
}
```
Context is built-in and ideal for low-frequency global values like theme or locale — but it re-renders all consumers on every value change, making it a poor fit for high-frequency state like a live feed. Redux adds a centralised store with time-travel debugging and middleware, suitable for complex state with many actors, but carries boilerplate overhead. Zustand is a lightweight alternative (2 kB) with a simple hook-based API; it only re-renders subscribers of the specific slice that changed, making it a good middle ground for medium-complexity apps. Reach for Context first, then Zustand, then Redux as complexity grows.
Context is built-in and ideal for low-frequency global values like theme or locale — but it re-renders all consumers on every value change, making it a poor fit for high-frequency state like a live feed. Redux adds a centralised store with time-travel debugging and middleware, suitable for complex state with many actors, but carries boilerplate overhead. Zustand is a lightweight alternative (2 kB) with a simple hook-based API; it only re-renders subscribers of the specific slice that changed, making it a good middle ground for medium-complexity apps. Reach for Context first, then Zustand, then Redux as complexity grows.
An error boundary is a class component that implements `static getDerivedStateFromError` and/or `componentDidCatch` to catch JavaScript errors in its child tree during rendering, and display a fallback UI instead of crashing. Hooks do not yet support error boundaries natively, so class components are still required (or a library like `react-error-boundary`). They do not catch errors in event handlers, async code, or server-side rendering.
An error boundary is a class component that implements `static getDerivedStateFromError` and/or `componentDidCatch` to catch JavaScript errors in its child tree during rendering, and display a fallback UI instead of crashing. Hooks do not yet support error boundaries natively, so class components are still required (or a library like `react-error-boundary`). They do not catch errors in event handlers, async code, or server-side rendering.
```jsx
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
render() {
return this.state.hasError
? <h2>Something went wrong.</h2>
: this.props.children;
}
}
```
`React.lazy` accepts a function that returns a dynamic `import()` and produces a lazy component. When the lazy component is first rendered, React suspends and shows the nearest `<Suspense fallback>` while the chunk loads. After the chunk resolves, React replaces the fallback with the real component. This reduces the initial bundle size by splitting vendor-heavy routes into separate chunks loaded on demand.
`React.lazy` accepts a function that returns a dynamic `import()` and produces a lazy component. When the lazy component is first rendered, React suspends and shows the nearest `<Suspense fallback>` while the chunk loads. After the chunk resolves, React replaces the fallback with the real component. This reduces the initial bundle size by splitting vendor-heavy routes into separate chunks loaded on demand.
```jsx
const Chart = React.lazy(() => import("./Chart"));
<Suspense fallback={<Spinner />}>
<Chart />
</Suspense>
```
React Router v6 replaces `<Switch>` with `<Routes>`, which has smarter relative path matching and always picks the best match instead of the first. Nested routes are declared declaratively inside `<Routes>` with `<Outlet>` for rendering children. `useHistory` was replaced by `useNavigate`. Route components are rendered via the `element` prop (accepting JSX) instead of `component` or `render`. The v6 API is significantly smaller and relies on hooks throughout.
React Router v6 replaces `<Switch>` with `<Routes>`, which has smarter relative path matching and always picks the best match instead of the first. Nested routes are declared declaratively inside `<Routes>` with `<Outlet>` for rendering children. `useHistory` was replaced by `useNavigate`. Route components are rendered via the `element` prop (accepting JSX) instead of `component` or `render`. The v6 API is significantly smaller and relies on hooks throughout.
Use the React DevTools Profiler tab to record a session and inspect which components rendered, how long each took, and why they rendered (changed props, state, or context). Look for components that render frequently or have a high self time. Also check the Flame Graph for deep render chains. In production, the React Profiler API (`<Profiler id onRender>`) lets you collect timing data and send it to analytics. Follow up with Chrome Performance and Lighthouse for Core Web Vitals (LCP, INP, CLS).
Use the React DevTools Profiler tab to record a session and inspect which components rendered, how long each took, and why they rendered (changed props, state, or context). Look for components that render frequently or have a high self time. Also check the Flame Graph for deep render chains. In production, the React Profiler API (`<Profiler id onRender>`) lets you collect timing data and send it to analytics. Follow up with Chrome Performance and Lighthouse for Core Web Vitals (LCP, INP, CLS).
Virtualization renders only the visible rows of a long list instead of all DOM nodes at once. Libraries like `react-window` and `react-virtual` maintain a small window of DOM nodes and swap in content as the user scrolls, keeping memory and layout cost constant regardless of list size. Use virtualization when a list has hundreds or thousands of items and you notice jank or high memory usage. For short lists (< 200 items) virtualization overhead usually outweighs the benefit.
Virtualization renders only the visible rows of a long list instead of all DOM nodes at once. Libraries like `react-window` and `react-virtual` maintain a small window of DOM nodes and swap in content as the user scrolls, keeping memory and layout cost constant regardless of list size. Use virtualization when a list has hundreds or thousands of items and you notice jank or high memory usage. For short lists (< 200 items) virtualization overhead usually outweighs the benefit.
React Testing Library (RTL) encourages testing from the user's perspective using queries like `getByRole`, `getByLabelText`, and `getByText` rather than component internals. Use `render` to mount the component, interact via `userEvent` or `fireEvent`, and assert against the DOM. RTL deliberately makes it hard to access component state or instance methods to keep tests behaviour-focused. Wrap async side effects in `await waitFor` or `findBy*` queries.
React Testing Library (RTL) encourages testing from the user's perspective using queries like `getByRole`, `getByLabelText`, and `getByText` rather than component internals. Use `render` to mount the component, interact via `userEvent` or `fireEvent`, and assert against the DOM. RTL deliberately makes it hard to access component state or instance methods to keep tests behaviour-focused. Wrap async side effects in `await waitFor` or `findBy*` queries.
```tsx
render(<LoginForm />);
await userEvent.type(screen.getByLabelText(/email/i), "a@b.com");
await userEvent.click(screen.getByRole("button", { name: /submit/i }));
expect(screen.getByText(/welcome/i)).toBeInTheDocument();
```
Snapshot tests serialise a component's rendered output (usually via `react-test-renderer` or RTL's `asFragment`) and store it in a `.snap` file. On subsequent runs, the output is compared against the stored snapshot — a mismatch fails the test. Snapshots are useful for catching unintended UI regressions in stable leaf components. They are noisy for rapidly changing UIs; large snapshots become hard to review. Prefer targeted assertions with RTL for interactive components and reserve snapshots for static, stable UI like icons or design tokens.
Snapshot tests serialise a component's rendered output (usually via `react-test-renderer` or RTL's `asFragment`) and store it in a `.snap` file. On subsequent runs, the output is compared against the stored snapshot — a mismatch fails the test. Snapshots are useful for catching unintended UI regressions in stable leaf components. They are noisy for rapidly changing UIs; large snapshots become hard to review. Prefer targeted assertions with RTL for interactive components and reserve snapshots for static, stable UI like icons or design tokens.
Key strategies: wrap pure leaf components with `React.memo`; memoize computed values with `useMemo`; stabilise callback references with `useCallback`; keep state as low in the tree as possible; split context into multiple providers so unrelated consumers do not re-render; use a selector-based state library (Zustand, Redux) that only notifies subscribers when their slice changes; and prefer composition (passing elements as props) over lifting state. Always profile before optimising — premature memoisation adds overhead.
Key strategies: wrap pure leaf components with `React.memo`; memoize computed values with `useMemo`; stabilise callback references with `useCallback`; keep state as low in the tree as possible; split context into multiple providers so unrelated consumers do not re-render; use a selector-based state library (Zustand, Redux) that only notifies subscribers when their slice changes; and prefer composition (passing elements as props) over lifting state. Always profile before optimising — premature memoisation adds overhead.
Fiber is the complete rewrite of React's reconciliation engine (shipped in React 16) that replaces the previous recursive, stack-based algorithm. Each unit of work is a "fiber" — a plain JS object representing a component instance — and work is broken into small units that can be paused, resumed, or aborted. This enables time-slicing (yielding to the browser between units) and priority scheduling. Fiber is the foundation for concurrent features like `useTransition` and streaming Suspense.
Fiber is the complete rewrite of React's reconciliation engine (shipped in React 16) that replaces the previous recursive, stack-based algorithm. Each unit of work is a "fiber" — a plain JS object representing a component instance — and work is broken into small units that can be paused, resumed, or aborted. This enables time-slicing (yielding to the browser between units) and priority scheduling. Fiber is the foundation for concurrent features like `useTransition` and streaming Suspense.
Before React 18, multiple state updates inside event handlers were batched into a single re-render, but updates inside `setTimeout`, Promises, or native event listeners triggered separate renders for each `setState` call. React 18 extends batching to all contexts automatically — multiple `setState` calls anywhere in the same microtask are grouped into one render. Use `flushSync` to opt out of batching and force an immediate synchronous render when needed.
Before React 18, multiple state updates inside event handlers were batched into a single re-render, but updates inside `setTimeout`, Promises, or native event listeners triggered separate renders for each `setState` call. React 18 extends batching to all contexts automatically — multiple `setState` calls anywhere in the same microtask are grouped into one render. Use `flushSync` to opt out of batching and force an immediate synchronous render when needed.
`useTransition` marks a state update as non-urgent, allowing React to keep the current UI responsive while preparing the new state in the background. It returns `[isPending, startTransition]`. Wrap low-priority updates (heavy list filtering, tab switching) in `startTransition` so that high-priority updates (typing, clicking) are not blocked. `isPending` is `true` while the deferred render is in progress, letting you show a subtle loading indicator without a full spinner.
`useTransition` marks a state update as non-urgent, allowing React to keep the current UI responsive while preparing the new state in the background. It returns `[isPending, startTransition]`. Wrap low-priority updates (heavy list filtering, tab switching) in `startTransition` so that high-priority updates (typing, clicking) are not blocked. `isPending` is `true` while the deferred render is in progress, letting you show a subtle loading indicator without a full spinner.
```jsx
const [isPending, startTransition] = useTransition();
startTransition(() => setQuery(value));
```
`useDeferredValue` accepts a value and returns a deferred version of it that lags behind when the CPU is busy — React keeps showing the old deferred value while the urgent render commits, then re-renders with the new deferred value when idle. Unlike `useTransition` (which wraps a setter call), `useDeferredValue` wraps a derived value, making it useful when you do not control the state that produces that value (e.g., a prop from a parent). Both achieve the same goal — prioritising UI responsiveness.
`useDeferredValue` accepts a value and returns a deferred version of it that lags behind when the CPU is busy — React keeps showing the old deferred value while the urgent render commits, then re-renders with the new deferred value when idle. Unlike `useTransition` (which wraps a setter call), `useDeferredValue` wraps a derived value, making it useful when you do not control the state that produces that value (e.g., a prop from a parent). Both achieve the same goal — prioritising UI responsiveness.
Concurrent rendering is a React 18 feature where React can work on multiple versions of the UI simultaneously. It can start rendering a state update, pause it (yielding to the browser or a higher-priority update), and either resume or discard the incomplete tree. This makes large updates non-blocking. Concurrent rendering is opt-in via `createRoot` and activated by features like `useTransition`, `useDeferredValue`, and streaming Suspense.
Concurrent rendering is a React 18 feature where React can work on multiple versions of the UI simultaneously. It can start rendering a state update, pause it (yielding to the browser or a higher-priority update), and either resume or discard the incomplete tree. This makes large updates non-blocking. Concurrent rendering is opt-in via `createRoot` and activated by features like `useTransition`, `useDeferredValue`, and streaming Suspense.
`flushSync(callback)` forces React to flush all state updates inside the callback synchronously and commit them to the DOM before returning. Because React 18 batches everything by default, `flushSync` is the escape hatch for cases where you need the DOM to reflect a state change immediately — for example, reading a DOM measurement after a setState, or third-party integrations expecting synchronous DOM updates. Overusing it defeats concurrent mode benefits.
`flushSync(callback)` forces React to flush all state updates inside the callback synchronously and commit them to the DOM before returning. Because React 18 batches everything by default, `flushSync` is the escape hatch for cases where you need the DOM to reflect a state change immediately — for example, reading a DOM measurement after a setState, or third-party integrations expecting synchronous DOM updates. Overusing it defeats concurrent mode benefits.
`useId` (React 18) generates a stable, unique string ID that is consistent between server and client renders, preventing hydration mismatches. Before `useId`, developers used random IDs or counters that differed between SSR and hydration. Use it to tie together form labels and inputs (`htmlFor` / `id`) or ARIA attributes when multiple instances of a component can appear on the same page.
jsx
function Field({ label }) {
const id = useId();
return (<><label htmlFor={id}>{label}</label><input id={id} /></>);
}
`useId` (React 18) generates a stable, unique string ID that is consistent between server and client renders, preventing hydration mismatches. Before `useId`, developers used random IDs or counters that differed between SSR and hydration. Use it to tie together form labels and inputs (`htmlFor` / `id`) or ARIA attributes when multiple instances of a component can appear on the same page.
```jsx
function Field({ label }) {
const id = useId();
return (<><label htmlFor={id}>{label}</label><input id={id} /></>);
}
```
`useLayoutEffect` fires synchronously after all DOM mutations but before the browser paints, so it blocks the visual update. Use it when you need to read layout (e.g., element dimensions) and immediately apply a DOM change to avoid a flicker. `useEffect` fires asynchronously after the browser has painted, making it non-blocking and suitable for data fetching, subscriptions, and logging. Server-side rendering always skips `useLayoutEffect`, generating a warning; guard with a `useIsomorphicLayoutEffect` pattern if you need SSR compatibility.
`useLayoutEffect` fires synchronously after all DOM mutations but before the browser paints, so it blocks the visual update. Use it when you need to read layout (e.g., element dimensions) and immediately apply a DOM change to avoid a flicker. `useEffect` fires asynchronously after the browser has painted, making it non-blocking and suitable for data fetching, subscriptions, and logging. Server-side rendering always skips `useLayoutEffect`, generating a warning; guard with a `useIsomorphicLayoutEffect` pattern if you need SSR compatibility.
`react-hook-form` uses uncontrolled inputs with refs internally, so typing does not cause the parent component to re-render on every keystroke. It stores form values in a mutable ref object and only triggers re-renders when validation state changes or on submit. This produces far fewer renders than a fully controlled form where every character update calls `setState`. The `Controller` wrapper bridges it to controlled third-party UI libraries when needed.
`react-hook-form` uses uncontrolled inputs with refs internally, so typing does not cause the parent component to re-render on every keystroke. It stores form values in a mutable ref object and only triggers re-renders when validation state changes or on submit. This produces far fewer renders than a fully controlled form where every character update calls `setState`. The `Controller` wrapper bridges it to controlled third-party UI libraries when needed.
Optimistic UI immediately applies the expected result of an async operation to the UI before the server confirms it, making interactions feel instant. On failure, you roll back to the previous state and show an error. Implement it by updating local state before the API call, catching errors, and reverting. TanStack Query's `onMutate`/`onError`/`onSettled` callbacks and React 19's `useOptimistic` hook automate this pattern.
Optimistic UI immediately applies the expected result of an async operation to the UI before the server confirms it, making interactions feel instant. On failure, you roll back to the previous state and show an error. Implement it by updating local state before the API call, catching errors, and reverting. TanStack Query's `onMutate`/`onError`/`onSettled` callbacks and React 19's `useOptimistic` hook automate this pattern.
TanStack Query is a server-state library that handles data fetching, caching, background refetching, pagination, and synchronisation for async data. It distinguishes server state (owned by the server, potentially stale) from client state (owned by the browser). It eliminates boilerplate `useEffect`-based data fetching, provides automatic stale-while-revalidate caching, request deduplication, and DevTools for cache inspection. Key concepts: `queryKey` for cache identity, `staleTime` for cache lifetime, and `invalidateQueries` to trigger refetches.
TanStack Query is a server-state library that handles data fetching, caching, background refetching, pagination, and synchronisation for async data. It distinguishes server state (owned by the server, potentially stale) from client state (owned by the browser). It eliminates boilerplate `useEffect`-based data fetching, provides automatic stale-while-revalidate caching, request deduplication, and DevTools for cache inspection. Key concepts: `queryKey` for cache identity, `staleTime` for cache lifetime, and `invalidateQueries` to trigger refetches.
Common patterns: (1) `useEffect` + local state — simple but error-prone and lacks caching; (2) TanStack Query / SWR — server state with caching, deduplication, and background refresh; (3) Remix/Next.js loader functions — fetch on the server before rendering, avoiding client waterfalls; (4) React Server Components — zero-bundle server fetching; (5) Relay — collocated GraphQL fragments. The right choice depends on whether data is user-specific, how stale it can be, and whether SSR is required.
Common patterns: (1) `useEffect` + local state — simple but error-prone and lacks caching; (2) TanStack Query / SWR — server state with caching, deduplication, and background refresh; (3) Remix/Next.js loader functions — fetch on the server before rendering, avoiding client waterfalls; (4) React Server Components — zero-bundle server fetching; (5) Relay — collocated GraphQL fragments. The right choice depends on whether data is user-specific, how stale it can be, and whether SSR is required.
Compound components are a set of components that share implicit state through Context rather than explicit prop drilling. A `<Select>` composed of `<Select.Option>` children is the canonical example — the parent `Select` owns the selected value, and each `Option` reads it from context to know if it is active. This pattern creates expressive, flexible APIs where the consumer controls layout while the parent controls logic.
Compound components are a set of components that share implicit state through Context rather than explicit prop drilling. A `<Select>` composed of `<Select.Option>` children is the canonical example — the parent `Select` owns the selected value, and each `Option` reads it from context to know if it is active. This pattern creates expressive, flexible APIs where the consumer controls layout while the parent controls logic.
Portals render into a DOM node outside the React root (usually `document.body`) while keeping the component inside the React tree. This means focus management, keyboard events, and context still work correctly. For a modal: use `ReactDOM.createPortal(<Modal />, document.getElementById("modal-root"))`. Combine with a `useEffect` to lock body scroll, manage focus trapping with `aria-modal` and `aria-label`, and restore focus to the trigger element on close.
Portals render into a DOM node outside the React root (usually `document.body`) while keeping the component inside the React tree. This means focus management, keyboard events, and context still work correctly. For a modal: use `ReactDOM.createPortal(<Modal />, document.getElementById("modal-root"))`. Combine with a `useEffect` to lock body scroll, manage focus trapping with `aria-modal` and `aria-label`, and restore focus to the trigger element on close.
Keep a parallel `errors` state object keyed by field name. Validate on change, on blur, or on submit depending on UX requirements. For submit validation, iterate over fields, populate `errors`, and bail early with `setErrors` if any are present. For real-time validation, debounce expensive checks (e.g., async username availability). Display errors inline next to their fields and associate them with inputs via `aria-describedby` for accessibility.
Keep a parallel `errors` state object keyed by field name. Validate on change, on blur, or on submit depending on UX requirements. For submit validation, iterate over fields, populate `errors`, and bail early with `setErrors` if any are present. For real-time validation, debounce expensive checks (e.g., async username availability). Display errors inline next to their fields and associate them with inputs via `aria-describedby` for accessibility.
React's diff algorithm first compares element types. If the type changes (e.g., `<div>` → `<span>` or `<Foo>` → `<Bar>`), React destroys the old tree entirely — unmounting all children and their state — and builds a fresh tree from scratch. This is why swapping between two different component types, even with similar props, resets all child state. For elements of the same type, React preserves the instance and updates only the changed attributes or props.
React's diff algorithm first compares element types. If the type changes (e.g., `<div>` → `<span>` or `<Foo>` → `<Bar>`), React destroys the old tree entirely — unmounting all children and their state — and builds a fresh tree from scratch. This is why swapping between two different component types, even with similar props, resets all child state. For elements of the same type, React preserves the instance and updates only the changed attributes or props.
`useMutation` handles create/update/delete operations. It does not cache results by default; instead, on success you call `queryClient.invalidateQueries` to refetch relevant cached queries or use `setQueryData` for optimistic updates. The hook provides `mutate` / `mutateAsync`, plus `isPending`, `isError`, and `isSuccess` states. Use `onMutate` for optimistic updates, `onError` to roll back, and `onSettled` to always invalidate regardless of outcome.
`useMutation` handles create/update/delete operations. It does not cache results by default; instead, on success you call `queryClient.invalidateQueries` to refetch relevant cached queries or use `setQueryData` for optimistic updates. The hook provides `mutate` / `mutateAsync`, plus `isPending`, `isError`, and `isSuccess` states. Use `onMutate` for optimistic updates, `onError` to roll back, and `onSettled` to always invalidate regardless of outcome.
The render props pattern passes a function as a prop that a component calls to produce its rendered output, giving the consumer full control over what is rendered while the providing component handles logic. It is still useful in library APIs (e.g., `<Downshift>`, `<Formik>`) where Hooks would require wrapping every consumer in a HOC. In application code, custom Hooks are almost always simpler and should be preferred.
The render props pattern passes a function as a prop that a component calls to produce its rendered output, giving the consumer full control over what is rendered while the providing component handles logic. It is still useful in library APIs (e.g., `<Downshift>`, `<Formik>`) where Hooks would require wrapping every consumer in a HOC. In application code, custom Hooks are almost always simpler and should be preferred.
React's `onChange` for inputs fires on every keypress, not just when the element loses focus as native DOM `change` does. This is actually mapped to the native `input` event under the hood, giving a real-time reactive update model. React normalises this across browsers. The native `onBlur` in React maps to the native `blur` event. Understanding this prevents bugs when mixing React-controlled and uncontrolled patterns.
React's `onChange` for inputs fires on every keypress, not just when the element loses focus as native DOM `change` does. This is actually mapped to the native `input` event under the hood, giving a real-time reactive update model. React normalises this across browsers. The native `onBlur` in React maps to the native `blur` event. Understanding this prevents bugs when mixing React-controlled and uncontrolled patterns.
`key` is typically used to identify list items so React can match old and new elements during reconciliation. A less obvious use is intentionally changing `key` on a component to force React to unmount the old instance and mount a fresh one — resetting all state and refs. This is cleaner than manually resetting state inside `useEffect` when, for example, a "new form" and "edit form" share the same component but require completely fresh state.
jsx
<UserForm key={userId} userId={userId} />
// Changing userId unmounts old form and mounts fresh one
`key` is typically used to identify list items so React can match old and new elements during reconciliation. A less obvious use is intentionally changing `key` on a component to force React to unmount the old instance and mount a fresh one — resetting all state and refs. This is cleaner than manually resetting state inside `useEffect` when, for example, a "new form" and "edit form" share the same component but require completely fresh state.
```jsx
<UserForm key={userId} userId={userId} />
// Changing userId unmounts old form and mounts fresh one
```
The simplest approach is the native `loading="lazy"` attribute on `<img>` elements, which browsers support for off-screen images. For finer control, use an `IntersectionObserver` in a custom hook — store the ref and a `isVisible` boolean in state, start with a placeholder, and swap in the real `src` once the element enters the viewport. Libraries like `react-intersection-observer` wrap this cleanly. In Next.js, `<Image>` handles lazy loading, responsive sizes, and WebP conversion automatically.
The simplest approach is the native `loading="lazy"` attribute on `<img>` elements, which browsers support for off-screen images. For finer control, use an `IntersectionObserver` in a custom hook — store the ref and a `isVisible` boolean in state, start with a placeholder, and swap in the real `src` once the element enters the viewport. Libraries like `react-intersection-observer` wrap this cleanly. In Next.js, `<Image>` handles lazy loading, responsive sizes, and WebP conversion automatically.
Stale-While-Revalidate (SWR) returns cached (stale) data immediately so the UI renders fast, then fetches fresh data in the background and updates the UI when it arrives. TanStack Query controls this via `staleTime` (how long data is considered fresh — no background fetch) and `gcTime` (how long unused data stays in cache). Setting `staleTime: 0` means every mount triggers a background refetch; `staleTime: Infinity` disables refetching entirely.
Stale-While-Revalidate (SWR) returns cached (stale) data immediately so the UI renders fast, then fetches fresh data in the background and updates the UI when it arrives. TanStack Query controls this via `staleTime` (how long data is considered fresh — no background fetch) and `gcTime` (how long unused data stays in cache). Setting `staleTime: 0` means every mount triggers a background refetch; `staleTime: Infinity` disables refetching entirely.
In concurrent mode (React 18 via `createRoot`), React may start rendering a component tree, pause it, and discard the partially rendered result without committing. This means your render function may be called multiple times for the same update. Side effects inside render (mutations, subscriptions) will break — only pure computations belong in render. Effects, which run after commit, are still guaranteed to run once per committed update (except StrictMode double-invoke in development).
In concurrent mode (React 18 via `createRoot`), React may start rendering a component tree, pause it, and discard the partially rendered result without committing. This means your render function may be called multiple times for the same update. Side effects inside render (mutations, subscriptions) will break — only pure computations belong in render. Effects, which run after commit, are still guaranteed to run once per committed update (except StrictMode double-invoke in development).
`useSyncExternalStore` (React 18) is the recommended way to subscribe to external stores (Redux, Zustand internals, browser APIs). It takes a `subscribe` function, a `getSnapshot` function, and an optional `getServerSnapshot`. React uses it to detect "tearing" (different components seeing different store snapshots in a concurrent render) and re-renders consistently. Before React 18, developers used `useEffect` + `useState` which was susceptible to tearing.
`useSyncExternalStore` (React 18) is the recommended way to subscribe to external stores (Redux, Zustand internals, browser APIs). It takes a `subscribe` function, a `getSnapshot` function, and an optional `getServerSnapshot`. React uses it to detect "tearing" (different components seeing different store snapshots in a concurrent render) and re-renders consistently. Before React 18, developers used `useEffect` + `useState` which was susceptible to tearing.
React's built-in context re-renders ALL consumers whenever the context value changes, even if the consuming component only uses a slice of that value. A selector pattern (popularised by `use-context-selector` or Zustand) lets components subscribe to only the piece they care about, skipping re-renders when other parts of the value change. Without this, large context objects (e.g., a full user profile) cause excessive re-renders throughout the tree.
React's built-in context re-renders ALL consumers whenever the context value changes, even if the consuming component only uses a slice of that value. A selector pattern (popularised by `use-context-selector` or Zustand) lets components subscribe to only the piece they care about, skipping re-renders when other parts of the value change. Without this, large context objects (e.g., a full user profile) cause excessive re-renders throughout the tree.
Associate every input with a visible `<label>` using `htmlFor` + `id` (or `useId`). For error messages, add `aria-describedby` pointing to the error element and `aria-invalid="true"` when validation fails. Use `role="alert"` or `aria-live="polite"` on error containers so screen readers announce them without page focus. Provide `fieldset`/`legend` for related groups. Ensure keyboard navigation works — no `tabIndex=-1` on interactive elements without a reason, and focus the first invalid field on submit failure.
Associate every input with a visible `<label>` using `htmlFor` + `id` (or `useId`). For error messages, add `aria-describedby` pointing to the error element and `aria-invalid="true"` when validation fails. Use `role="alert"` or `aria-live="polite"` on error containers so screen readers announce them without page focus. Provide `fieldset`/`legend` for related groups. Ensure keyboard navigation works — no `tabIndex=-1` on interactive elements without a reason, and focus the first invalid field on submit failure.
`React.memo` is a component-level HOC that memoizes the entire rendered output of a function component based on shallow prop equality. `useMemo` is a Hook that memoizes a computed value inside a component. Use `React.memo` to prevent a child component from re-rendering when its parent re-renders but its props have not changed. Use `useMemo` to avoid recomputing an expensive value inside a component's render. They solve different problems and can be combined.
`React.memo` is a component-level HOC that memoizes the entire rendered output of a function component based on shallow prop equality. `useMemo` is a Hook that memoizes a computed value inside a component. Use `React.memo` to prevent a child component from re-rendering when its parent re-renders but its props have not changed. Use `useMemo` to avoid recomputing an expensive value inside a component's render. They solve different problems and can be combined.
`act()` is a test helper that ensures all state updates, effects, and async operations triggered by a user interaction are flushed and committed before assertions run. RTL wraps most interactions in `act()` automatically. You need to call it manually when testing async state changes triggered outside of React's event system (e.g., a resolved Promise in a `useEffect`). Forgetting `act()` leads to warnings and unreliable tests that pass or fail based on timing.
`act()` is a test helper that ensures all state updates, effects, and async operations triggered by a user interaction are flushed and committed before assertions run. RTL wraps most interactions in `act()` automatically. You need to call it manually when testing async state changes triggered outside of React's event system (e.g., a resolved Promise in a `useEffect`). Forgetting `act()` leads to warnings and unreliable tests that pass or fail based on timing.
RSC are components that render exclusively on the server and never ship their code to the browser. They can directly await databases, file systems, and secrets without exposing them client-side, and their output (a serialised React tree) is streamed to the client. Server Components cannot use state, effects, or browser APIs. Client Components (`"use client"` directive) run on both server (for initial HTML) and client (for hydration and interaction). The boundary between them is explicit: a Server Component can import a Client Component, but a Client Component cannot import a Server Component — only receive one as a prop.
RSC are components that render exclusively on the server and never ship their code to the browser. They can directly await databases, file systems, and secrets without exposing them client-side, and their output (a serialised React tree) is streamed to the client. Server Components cannot use state, effects, or browser APIs. Client Components (`"use client"` directive) run on both server (for initial HTML) and client (for hydration and interaction). The boundary between them is explicit: a Server Component can import a Client Component, but a Client Component cannot import a Server Component — only receive one as a prop.
With streaming SSR, the server sends HTML in chunks using HTTP chunked transfer encoding. Suspense boundaries act as flush points: React sends the shell (layout, nav) immediately, then streams each Suspense-wrapped subtree as its data resolves. On the client, React hydrates each chunk progressively as it arrives, keeping the page interactive before the full HTML is loaded. In Next.js App Router, each `loading.tsx` file creates a Suspense boundary and each `page.tsx` can be an async Server Component that suspends on data fetches.
With streaming SSR, the server sends HTML in chunks using HTTP chunked transfer encoding. Suspense boundaries act as flush points: React sends the shell (layout, nav) immediately, then streams each Suspense-wrapped subtree as its data resolves. On the client, React hydrates each chunk progressively as it arrives, keeping the page interactive before the full HTML is loaded. In Next.js App Router, each `loading.tsx` file creates a Suspense boundary and each `page.tsx` can be an async Server Component that suspends on data fetches.
App Router treats every file in the `app/` directory as a Server Component by default. Layouts are persistent Server Components that wrap multiple pages without re-mounting on navigation. Page components are async and can directly `await` database calls. `"use client"` marks interactive subtrees. Route segments support `loading.tsx` (Suspense fallback), `error.tsx` (Error Boundary), and `not-found.tsx`. Data fetching is colocated with the component that needs it, eliminating prop drilling and waterfall API calls typical of CSR apps.
App Router treats every file in the `app/` directory as a Server Component by default. Layouts are persistent Server Components that wrap multiple pages without re-mounting on navigation. Page components are async and can directly `await` database calls. `"use client"` marks interactive subtrees. Route segments support `loading.tsx` (Suspense fallback), `error.tsx` (Error Boundary), and `not-found.tsx`. Data fetching is colocated with the component that needs it, eliminating prop drilling and waterfall API calls typical of CSR apps.
The React Compiler (formerly React Forget) is an ahead-of-time Babel/SWC transform that automatically inserts `useMemo`, `useCallback`, and `React.memo` where the compiler proves they are safe and beneficial. It analyses data flow and mutability at compile time, eliminating the need for manual memoisation. The compiler guarantees correctness by only memoising pure computations; it deliberately skips memoising anything with side effects. Rolled out in Meta's production apps in 2024 and available as `babel-plugin-react-compiler` in React 19.
The React Compiler (formerly React Forget) is an ahead-of-time Babel/SWC transform that automatically inserts `useMemo`, `useCallback`, and `React.memo` where the compiler proves they are safe and beneficial. It analyses data flow and mutability at compile time, eliminating the need for manual memoisation. The compiler guarantees correctness by only memoising pure computations; it deliberately skips memoising anything with side effects. Rolled out in Meta's production apps in 2024 and available as `babel-plugin-react-compiler` in React 19.
The core rule: once you cross into a `"use client"` boundary, everything below is a Client Component. A Server Component can pass serialisable props (strings, numbers, plain objects, arrays, Promises) to Client Components — functions and class instances cannot cross the boundary. You can pass a Server Component as a `children` prop to a Client Component (the Server Component renders on the server; the Client Component receives the already-rendered React node as a prop). Context cannot be created in Server Components but can be created in Client Components and consumed further down.
The core rule: once you cross into a `"use client"` boundary, everything below is a Client Component. A Server Component can pass serialisable props (strings, numbers, plain objects, arrays, Promises) to Client Components — functions and class instances cannot cross the boundary. You can pass a Server Component as a `children` prop to a Client Component (the Server Component renders on the server; the Client Component receives the already-rendered React node as a prop). Context cannot be created in Server Components but can be created in Client Components and consumed further down.
React 19 introduces Actions — async functions passed to the `action` prop of `<form>` or triggered via `useActionState` (formerly `useFormState`). React wraps them in a transition automatically, provides `isPending` state, and handles errors gracefully. `useActionState` returns `[state, formAction, isPending]` where `state` is the last value returned by the action. This pattern replaces the typical `onSubmit` + `useState` + `useTransition` combo with a single, composable primitive tightly integrated with the new Server Actions mechanism in Next.js.
React 19 introduces Actions — async functions passed to the `action` prop of `<form>` or triggered via `useActionState` (formerly `useFormState`). React wraps them in a transition automatically, provides `isPending` state, and handles errors gracefully. `useActionState` returns `[state, formAction, isPending]` where `state` is the last value returned by the action. This pattern replaces the typical `onSubmit` + `useState` + `useTransition` combo with a single, composable primitive tightly integrated with the new Server Actions mechanism in Next.js.
`use(promise)` unwraps a Promise inside a component's render function, suspending if the Promise is pending and throwing if it rejects (caught by the nearest Error Boundary). Unlike `await` in an async component, `use` can be called conditionally and inside loops. It also accepts a Context object as an argument, replacing `useContext`. When used with cached server-side Promises (e.g., from Next.js `cache()`), it enables fine-grained streaming: multiple components can `use()` the same promise and React deduplicates the request.
jsx
const data = use(fetchUserPromise); // suspends until resolved
`use(promise)` unwraps a Promise inside a component's render function, suspending if the Promise is pending and throwing if it rejects (caught by the nearest Error Boundary). Unlike `await` in an async component, `use` can be called conditionally and inside loops. It also accepts a Context object as an argument, replacing `useContext`. When used with cached server-side Promises (e.g., from Next.js `cache()`), it enables fine-grained streaming: multiple components can `use()` the same promise and React deduplicates the request.
```jsx
const data = use(fetchUserPromise); // suspends until resolved
```
`useOptimistic(state, updateFn)` returns an optimistic state and an `addOptimistic` dispatcher. While an async action (transition) is in flight, the optimistic value is shown; once the action settles, React reverts to the actual `state`. The `updateFn` merges the optimistic update into the current state — useful for appending an item to a list before the server confirms it. On error, the optimistic value is automatically discarded and the real state is restored, removing the manual rollback boilerplate.
`useOptimistic(state, updateFn)` returns an optimistic state and an `addOptimistic` dispatcher. While an async action (transition) is in flight, the optimistic value is shown; once the action settles, React reverts to the actual `state`. The `updateFn` merges the optimistic update into the current state — useful for appending an item to a list before the server confirms it. On error, the optimistic value is automatically discarded and the real state is restored, removing the manual rollback boilerplate.
```jsx
const [optimisticMessages, addOptimistic] = useOptimistic(messages,
(state, newMsg) => [...state, newMsg]
);
```
Hydration mismatches occur when the HTML the server renders differs from what React produces during client-side hydration. Common causes: rendering `Date.now()` or `Math.random()` in render, accessing `window`/`localStorage` during SSR, or using browser-only APIs. Fixes: guard browser checks with `typeof window !== "undefined"` or `useEffect`; use `suppressHydrationWarning` sparingly for intentional differences (e.g., timestamps); use `useId` instead of manual IDs; defer rendering dynamic content with `useState` + `useEffect` so it only runs after hydration.
Hydration mismatches occur when the HTML the server renders differs from what React produces during client-side hydration. Common causes: rendering `Date.now()` or `Math.random()` in render, accessing `window`/`localStorage` during SSR, or using browser-only APIs. Fixes: guard browser checks with `typeof window !== "undefined"` or `useEffect`; use `suppressHydrationWarning` sparingly for intentional differences (e.g., timestamps); use `useId` instead of manual IDs; defer rendering dynamic content with `useState` + `useEffect` so it only runs after hydration.
Concurrent rendering means React may render a component multiple times for the same update before committing. Renders can be interrupted and discarded. This breaks assumptions that render side effects (API calls in render, mutable refs written during render) only happen once. The commit phase (effects, DOM mutations) is still synchronous and happens exactly once per committed update. StrictMode double-invokes renders in development to surface non-idempotent render logic. The practical rule: render must be a pure function of props, state, and context.
Concurrent rendering means React may render a component multiple times for the same update before committing. Renders can be interrupted and discarded. This breaks assumptions that render side effects (API calls in render, mutable refs written during render) only happen once. The commit phase (effects, DOM mutations) is still synchronous and happens exactly once per committed update. StrictMode double-invokes renders in development to surface non-idempotent render logic. The practical rule: render must be a pure function of props, state, and context.
React's scheduler (the `scheduler` package) uses a min-heap priority queue. Each unit of work has a priority lane (syncLane, inputContinuousLane, defaultLane, transitionLane, idleLane). The scheduler requests animation frames or uses `MessageChannel` to yield to the browser between chunks of work, checking if the current 5ms time slice has expired. High-priority user events (click, input) run in the sync lane and are never interrupted. Transitions use a lower lane and can be preempted by higher-priority work.
React's scheduler (the `scheduler` package) uses a min-heap priority queue. Each unit of work has a priority lane (syncLane, inputContinuousLane, defaultLane, transitionLane, idleLane). The scheduler requests animation frames or uses `MessageChannel` to yield to the browser between chunks of work, checking if the current 5ms time slice has expired. High-priority user events (click, input) run in the sync lane and are never interrupted. Transitions use a lower lane and can be preempted by higher-priority work.
React maintains two trees: the current (committed) fiber tree and the work-in-progress tree built during a render. Each fiber has `child`, `sibling`, and `return` pointers forming a linked list that can be traversed iteratively rather than recursively. During the render (reconciliation) phase, React walks the work-in-progress tree, creating, updating, or deleting fibers. The commit phase applies changes: `useLayoutEffect` cleanups and insertions happen synchronously; `useEffect` cleanups and callbacks are queued for the microtask after paint. Lanes (React 18) assign priority to each pending update so the scheduler knows which work to do first.
React maintains two trees: the current (committed) fiber tree and the work-in-progress tree built during a render. Each fiber has `child`, `sibling`, and `return` pointers forming a linked list that can be traversed iteratively rather than recursively. During the render (reconciliation) phase, React walks the work-in-progress tree, creating, updating, or deleting fibers. The commit phase applies changes: `useLayoutEffect` cleanups and insertions happen synchronously; `useEffect` cleanups and callbacks are queued for the microtask after paint. Lanes (React 18) assign priority to each pending update so the scheduler knows which work to do first.
`react-reconciler` is the package that powers both ReactDOM and React Native. You supply a host config object implementing methods like `createInstance`, `appendChildToContainer`, `commitUpdate`, `removeChild`, etc., targeting your custom output (canvas, PDF, terminal, game engine). React handles diffing, Suspense, and context; your host config handles the actual mutations. Libraries like `react-three-fiber` (Three.js), `ink` (terminal), and `react-pdf` use this approach. The host config separates React's tree management from the rendering back-end.
`react-reconciler` is the package that powers both ReactDOM and React Native. You supply a host config object implementing methods like `createInstance`, `appendChildToContainer`, `commitUpdate`, `removeChild`, etc., targeting your custom output (canvas, PDF, terminal, game engine). React handles diffing, Suspense, and context; your host config handles the actual mutations. Libraries like `react-three-fiber` (Three.js), `ink` (terminal), and `react-pdf` use this approach. The host config separates React's tree management from the rendering back-end.
Use RTL's `findBy*` queries which return Promises and internally poll until the element appears. For Suspense + data fetching, mock the async module (e.g., `vi.mock("./api")`), resolve the mock in the test, and wrap assertions in `await waitFor`. When testing with MSW (Mock Service Worker), the network layer is intercepted in both Node.js tests and the browser. For React 18 concurrent mode tests, RTL wraps renders in `act()` and handles batching. Avoid `screen.getBy*` on elements that are behind a Suspense boundary before the promise resolves.
Use RTL's `findBy*` queries which return Promises and internally poll until the element appears. For Suspense + data fetching, mock the async module (e.g., `vi.mock("./api")`), resolve the mock in the test, and wrap assertions in `await waitFor`. When testing with MSW (Mock Service Worker), the network layer is intercepted in both Node.js tests and the browser. For React 18 concurrent mode tests, RTL wraps renders in `act()` and handles batching. Avoid `screen.getBy*` on elements that are behind a Suspense boundary before the promise resolves.
When a modal opens, move focus to the first interactive element inside it using a ref and `element.focus()` in a `useEffect`. Trap focus within the modal by intercepting `Tab` and `Shift+Tab` keydown events and cycling through focusable elements. When the modal closes, restore focus to the trigger element using a ref captured before opening. On route transitions in SPA navigation, move focus to the page heading or a skip-nav link so screen reader users know the page changed. Use `aria-modal`, `role="dialog"`, and `aria-labelledby` on modal containers.
When a modal opens, move focus to the first interactive element inside it using a ref and `element.focus()` in a `useEffect`. Trap focus within the modal by intercepting `Tab` and `Shift+Tab` keydown events and cycling through focusable elements. When the modal closes, restore focus to the trigger element using a ref captured before opening. On route transitions in SPA navigation, move focus to the page heading or a skip-nav link so screen reader users know the page changed. Use `aria-modal`, `role="dialog"`, and `aria-labelledby` on modal containers.
Micro-frontends split a large React SPA into independently deployable sub-apps owned by separate teams. Approaches: Module Federation (Webpack 5) allows runtime sharing of components across separately deployed builds; iframes provide hard isolation but poor UX; single-spa orchestrates multiple frameworks on one page. Trade-offs: independent deployment and team autonomy vs runtime complexity, duplicated React instances, shared routing conflicts, and version mismatch bugs. Shared design systems and a common auth layer are required to maintain UX consistency.
Micro-frontends split a large React SPA into independently deployable sub-apps owned by separate teams. Approaches: Module Federation (Webpack 5) allows runtime sharing of components across separately deployed builds; iframes provide hard isolation but poor UX; single-spa orchestrates multiple frameworks on one page. Trade-offs: independent deployment and team autonomy vs runtime complexity, duplicated React instances, shared routing conflicts, and version mismatch bugs. Shared design systems and a common auth layer are required to maintain UX consistency.
Webpack 5 Module Federation allows a "host" app to load components or modules from a "remote" app at runtime — without including them in the host's bundle. The remote exposes named exports in its `webpack.config.js`; the host declares the remote URL and consumes it with a lazy `import()`. Shared dependencies (React, ReactDOM) are listed in the `shared` config to prevent duplicate instances. In React, lazy-loaded federated modules are wrapped in `React.lazy` + `Suspense`. The main risk is runtime version conflicts and loading failures requiring error boundaries.
Webpack 5 Module Federation allows a "host" app to load components or modules from a "remote" app at runtime — without including them in the host's bundle. The remote exposes named exports in its `webpack.config.js`; the host declares the remote URL and consumes it with a lazy `import()`. Shared dependencies (React, ReactDOM) are listed in the `shared` config to prevent duplicate instances. In React, lazy-loaded federated modules are wrapped in `React.lazy` + `Suspense`. The main risk is runtime version conflicts and loading failures requiring error boundaries.
XState models UI state as explicit finite state machines or statecharts with defined states, transitions, guards, and actions. In React, `useMachine(machine)` from `@xstate/react` returns `[state, send]`. The machine lives outside the component, making the full state logic unit-testable without rendering. Complex flows like multi-step wizards, optimistic mutations, and WebSocket reconnection logic are much easier to reason about in a statechart than in nested `useState` booleans. `state.matches("submitting")` replaces multiple `isLoading`, `isError`, `isSuccess` flags.
XState models UI state as explicit finite state machines or statecharts with defined states, transitions, guards, and actions. In React, `useMachine(machine)` from `@xstate/react` returns `[state, send]`. The machine lives outside the component, making the full state logic unit-testable without rendering. Complex flows like multi-step wizards, optimistic mutations, and WebSocket reconnection logic are much easier to reason about in a statechart than in nested `useState` booleans. `state.matches("submitting")` replaces multiple `isLoading`, `isError`, `isSuccess` flags.
Key concerns: (1) API consistency — props like `size`, `variant`, `intent` follow predictable conventions; (2) Composition — components accept `className`, `style`, and `as` (polymorphic) props for extension; (3) Accessibility — every component ships with correct ARIA roles, keyboard support, and focus styles; (4) Token-driven — spacing, color, typography come from design tokens (CSS custom properties), not hardcoded values; (5) Tree-shaking — each component is a separate entry to avoid large bundle imports; (6) Versioning and changelog discipline to avoid breaking consumers.
Key concerns: (1) API consistency — props like `size`, `variant`, `intent` follow predictable conventions; (2) Composition — components accept `className`, `style`, and `as` (polymorphic) props for extension; (3) Accessibility — every component ships with correct ARIA roles, keyboard support, and focus styles; (4) Token-driven — spacing, color, typography come from design tokens (CSS custom properties), not hardcoded values; (5) Tree-shaking — each component is a separate entry to avoid large bundle imports; (6) Versioning and changelog discipline to avoid breaking consumers.
Combine virtualization (`react-virtual` or `react-window`) with pagination. The virtualiser renders only visible rows using absolute positioning; as the user scrolls near the bottom, an `IntersectionObserver` on a sentinel element triggers the next page fetch. Use TanStack Query's `useInfiniteQuery` for cache management and background refetching. Avoid layout thrash by reading scroll metrics in `useLayoutEffect` and setting dimensions with CSS instead of JS. Debounce scroll handlers and use `will-change: transform` on the scroll container for GPU compositing.
Combine virtualization (`react-virtual` or `react-window`) with pagination. The virtualiser renders only visible rows using absolute positioning; as the user scrolls near the bottom, an `IntersectionObserver` on a sentinel element triggers the next page fetch. Use TanStack Query's `useInfiniteQuery` for cache management and background refetching. Avoid layout thrash by reading scroll metrics in `useLayoutEffect` and setting dimensions with CSS instead of JS. Debounce scroll handlers and use `will-change: transform` on the scroll container for GPU compositing.
Create the WebSocket in a `useEffect` (or custom hook), listen for `message` events to update state via `setState` or a store, and close the connection in the cleanup function. Reconnection logic belongs in a ref-tracked retry loop or a state machine (XState). Avoid re-creating the socket on every render by using a `useRef` to hold the instance. For global WS connections shared across the app, lift the socket into a Context or Zustand store and subscribe individual components to the slice they care about.
Create the WebSocket in a `useEffect` (or custom hook), listen for `message` events to update state via `setState` or a store, and close the connection in the cleanup function. Reconnection logic belongs in a ref-tracked retry loop or a state machine (XState). Avoid re-creating the socket on every render by using a `useRef` to hold the instance. For global WS connections shared across the app, lift the socket into a Context or Zustand store and subscribe individual components to the slice they care about.
CSR (Client-Side Rendering): full HTML is a shell; JS fetches and renders data in the browser — poor SEO and slow LCP. SSR (Server-Side Rendering): full HTML on every request — good SEO, slower TTFB under load. ISR (Incremental Static Regeneration, Next.js): pre-rendered at build time with a `revalidate` window; serves cached HTML and regenerates in the background — best for content that changes infrequently. Streaming SSR: sends HTML progressively as data resolves — combines fast TTFB with server-rendered content and avoids full-page blocking. RSC + streaming is the recommended pattern for new Next.js apps.
CSR (Client-Side Rendering): full HTML is a shell; JS fetches and renders data in the browser — poor SEO and slow LCP. SSR (Server-Side Rendering): full HTML on every request — good SEO, slower TTFB under load. ISR (Incremental Static Regeneration, Next.js): pre-rendered at build time with a `revalidate` window; serves cached HTML and regenerates in the background — best for content that changes infrequently. Streaming SSR: sends HTML progressively as data resolves — combines fast TTFB with server-rendered content and avoids full-page blocking. RSC + streaming is the recommended pattern for new Next.js apps.
The `<Profiler id="MyComponent" onRender={callback}>` component instruments a subtree and calls `onRender` with timing data on every commit: `id`, `phase` ("mount" or "update"), `actualDuration` (render time), `baseDuration` (render time without memoization), `startTime`, and `commitTime`. Send this data to your analytics service to build real-user performance dashboards. It is available in production builds (unlike React DevTools Profiler) but adds a small overhead, so scope it to specific components or users rather than the whole app.
The `<Profiler id="MyComponent" onRender={callback}>` component instruments a subtree and calls `onRender` with timing data on every commit: `id`, `phase` ("mount" or "update"), `actualDuration` (render time), `baseDuration` (render time without memoization), `startTime`, and `commitTime`. Send this data to your analytics service to build real-user performance dashboards. It is available in production builds (unlike React DevTools Profiler) but adds a small overhead, so scope it to specific components or users rather than the whole app.
React uses `key` to match elements between renders. When `key` changes, React unmounts the old component instance (running cleanup effects) and mounts an entirely new one with fresh state — regardless of whether the component type stayed the same. This is a clean alternative to `useEffect` dependencies that try to reset multiple state slices: instead of `useEffect(() => { resetForm(); }, [userId])`, render `<UserForm key={userId} />` and let React's lifecycle do the reset. Be careful: this causes an unmount/mount, which may cause a visible flash.
React uses `key` to match elements between renders. When `key` changes, React unmounts the old component instance (running cleanup effects) and mounts an entirely new one with fresh state — regardless of whether the component type stayed the same. This is a clean alternative to `useEffect` dependencies that try to reset multiple state slices: instead of `useEffect(() => { resetForm(); }, [userId])`, render `<UserForm key={userId} />` and let React's lifecycle do the reset. Be careful: this causes an unmount/mount, which may cause a visible flash.
React wraps native DOM events in synthetic event objects that normalise cross-browser differences and pool objects (in older versions) for performance. Before React 17, all event handlers were delegated to `document`, which caused conflicts when embedding React inside non-React apps or other React versions. React 17 moved delegation from `document` to the root DOM container (`ReactDOM.createRoot` node), enabling safe co-existence of multiple React trees. React 18 does not pool synthetic events, so accessing event properties asynchronously is safe without `event.persist()`.
React wraps native DOM events in synthetic event objects that normalise cross-browser differences and pool objects (in older versions) for performance. Before React 17, all event handlers were delegated to `document`, which caused conflicts when embedding React inside non-React apps or other React versions. React 17 moved delegation from `document` to the root DOM container (`ReactDOM.createRoot` node), enabling safe co-existence of multiple React trees. React 18 does not pool synthetic events, so accessing event properties asynchronously is safe without `event.persist()`.
LCP: server-render above-the-fold content, preload LCP image with `<link rel="preload">`, avoid lazy-loading hero images. INP (Interaction to Next Paint): break up long tasks with `useTransition` and `useDeferredValue`; avoid heavy synchronous JS in event handlers; profile with Chrome's Interaction tracing in the Performance panel. CLS: reserve space for images, ads, and async content with explicit `width`/`height` attributes or CSS aspect-ratio; avoid inserting content above existing content after load. Use Next.js `<Image>` to get automatic sizing and lazy loading, and monitor with `web-vitals` library.
LCP: server-render above-the-fold content, preload LCP image with `<link rel="preload">`, avoid lazy-loading hero images. INP (Interaction to Next Paint): break up long tasks with `useTransition` and `useDeferredValue`; avoid heavy synchronous JS in event handlers; profile with Chrome's Interaction tracing in the Performance panel. CLS: reserve space for images, ads, and async content with explicit `width`/`height` attributes or CSS aspect-ratio; avoid inserting content above existing content after load. Use Next.js `<Image>` to get automatic sizing and lazy loading, and monitor with `web-vitals` library.
React Native uses the same React core (reconciler, hooks, context) but replaces the DOM renderer with a native mobile renderer. Business logic (state management, custom hooks, data fetching, form validation) is fully portable. UI components are not — `<div>` becomes `<View>`, `<span>` → `<Text>`, CSS → StyleSheet. React Native Web bridges the gap by mapping RN components to DOM elements, enabling true cross-platform component libraries. Nx or Turborepo monorepos with platform-specific entry points are the standard architecture for sharing code across React web and React Native.
React Native uses the same React core (reconciler, hooks, context) but replaces the DOM renderer with a native mobile renderer. Business logic (state management, custom hooks, data fetching, form validation) is fully portable. UI components are not — `<div>` becomes `<View>`, `<span>` → `<Text>`, CSS → StyleSheet. React Native Web bridges the gap by mapping RN components to DOM elements, enabling true cross-platform component libraries. Nx or Turborepo monorepos with platform-specific entry points are the standard architecture for sharing code across React web and React Native.
A naive tree diff is O(n³) — comparing all nodes across two trees. React achieves O(n) by applying two heuristics: (1) Elements of different types produce completely different trees (no cross-type matching); (2) Developers can hint stable identity using `key`. These heuristics mean React only compares nodes at the same depth and position in the tree, scanning the list once. This works well for typical UI trees but means React cannot reuse components across different parent types even if they are structurally identical.
A naive tree diff is O(n³) — comparing all nodes across two trees. React achieves O(n) by applying two heuristics: (1) Elements of different types produce completely different trees (no cross-type matching); (2) Developers can hint stable identity using `key`. These heuristics mean React only compares nodes at the same depth and position in the tree, scanning the list once. This works well for typical UI trees but means React cannot reuse components across different parent types even if they are structurally identical.
Define a budget in `lighthouserc.js` or `bundlewatch.json`: e.g., main JS chunk ≤ 200 kB gzipped, LCP ≤ 2.5 s, total page weight ≤ 500 kB. In CI, run `bundlewatch` against the webpack stats file to fail builds that exceed size limits. Run Lighthouse CI with `lhci autorun` against a staging Vercel URL to enforce Core Web Vitals scores. Use `webpack-bundle-analyzer` locally to identify large dependencies. Set `performance.hints: "error"` in Webpack config to block builds that produce oversized assets.
Define a budget in `lighthouserc.js` or `bundlewatch.json`: e.g., main JS chunk ≤ 200 kB gzipped, LCP ≤ 2.5 s, total page weight ≤ 500 kB. In CI, run `bundlewatch` against the webpack stats file to fail builds that exceed size limits. Run Lighthouse CI with `lhci autorun` against a staging Vercel URL to enforce Core Web Vitals scores. Use `webpack-bundle-analyzer` locally to identify large dependencies. Set `performance.hints: "error"` in Webpack config to block builds that produce oversized assets.
Server Actions are async functions marked with `"use server"` that run on the server and can be called directly from Client Components. Next.js assigns each action a unique encrypted ID; the client calls a POST endpoint with that ID and serialised arguments. The response is a serialised React tree diff. Server Actions integrate with React's transition system — calling an action automatically wraps it in a transition, enabling `isPending` state and preventing partial UI updates. They support progressive enhancement: `<form action={serverAction}>` works without JavaScript.
Server Actions are async functions marked with `"use server"` that run on the server and can be called directly from Client Components. Next.js assigns each action a unique encrypted ID; the client calls a POST endpoint with that ID and serialised arguments. The response is a serialised React tree diff. Server Actions integrate with React's transition system — calling an action automatically wraps it in a transition, enabling `isPending` state and preventing partial UI updates. They support progressive enhancement: `<form action={serverAction}>` works without JavaScript.
Frequently Asked Questions
Should I learn Next.js for React interviews?
If the role lists it, yes — App Router, Server Components, and data fetching patterns are commonly probed. Otherwise, focus on vanilla React.
Which state manager to learn?
Most interviews assume React built-ins + one of Zustand, Redux Toolkit, or TanStack Query. Know the problems each solves.
How important are Server Components?
Growing fast. Understand the client/server split, streaming, and when to reach for use client vs use server.
What about testing?
React Testing Library and Playwright are standard. Know when to test behavior vs implementation details.
Do I need to know React 19 features?
Know the highlights: use() hook, Actions, optimistic updates, and the new compiler. Most codebases are still React 18.