React Interview Questions

50+ React interview questions and answers in quiz-style format, answered by ex-FAANG interviewers
Questions and solutions by ex-interviewers
Covers critical topics

In real-world scenarios, mastering React goes far beyond just building components. It’s about creating efficient, reusable, and performant applications. React interviewers typically focus on key areas such as:

  • Component Lifecycle: Understanding how components mount, update, and unmount is crucial for managing UI and state.
  • State and Props Management: Knowing when and how to use props, state, and context to share data across components.
  • Hooks: Leveraging React hooks like useState, useEffect, and useReducer to simplify logic and manage side effects.
  • Performance Optimization: Efficient rendering, memoization, and handling large datasets.
  • Testing: Writing robust tests for React components using tools like Jest and React Testing Library.
  • Routing: Managing views and navigation in single-page applications with React Router.

Below, you’ll find 50+ expertly curated questions covering everything from component lifecycle and state management to hooks and performance optimization. Each question includes:

  • Quick answers (TL;DR): Clear, concise responses to help you answer confidently.
  • In-depth explanations: Detailed insights to ensure you fully understand each concept.

Best of all, our list is crafted by senior and staff engineers from top tech companies, not unverified or AI-generated content. Don’t waste time. Prepare with real, experienced-backed React interview questions!

If you're looking for React coding questions -We've got you covered as well, with:
Javascript coding
  • 90+ React coding interview questions
  • An in-browser coding workspace that mimics real interview conditions
  • Reference solutions from ex-interviewers at Big Tech companies
  • Instant UI preview for UI questions
Get Started
Join 50,000+ engineers

What is React? Describe the benefits of React

Topics
React

TL;DR

React is an open-source library maintained by Meta and the community for building user interfaces from components. Components describe UI declaratively from props, state, and context; React reconciles those descriptions and commits the necessary changes through renderers such as React DOM and React Native. Its main benefits are composability, explicit one-way data flow, reusable stateful logic through Hooks, support for client and server rendering architectures, and a broad ecosystem.

Key characteristics of React:

  • Declarative: You describe the desired UI from data, and the renderer coordinates the required host updates.
  • Component-based: Build reusable and modular UI elements (components) that manage their own state and logic.
  • Reconciliation: React compares element descriptions and commits the necessary renderer-specific changes. These descriptions are not a copy of the browser DOM.
  • JSX: While not mandatory, JSX is a syntax extension for expressing element structure alongside JavaScript values and control flow.

What is React?

React is an open-source JavaScript library maintained by Meta and the community for building user interfaces. It can power small interactive widgets, single-page applications, native applications through React Native, and server-rendered applications through a framework. React lets developers compose components that derive their output from props, state, and context.

Modern React is function-component- and Hooks-oriented. JSX, an HTML-like syntax extension to JavaScript, is the standard way to describe UI. React 19 added stable Server Components, the use API for reading promises and context, and Actions for form and data mutations. React Compiler 1.0 is a separate, optional build-time optimizer that can memoize compatible components and values when enabled.

Benefits of React

React's benefits come from its component model, declarative rendering, renderer ecosystem, and scheduling capabilities.

1. Component-based architecture

React encourages breaking a UI into reusable components. A component can encapsulate rendering, local state, and related behavior, which supports:

  • Modularity and reuse: A component with an explicit prop contract can be composed in multiple features or applications.
  • Local reasoning: Keeping related rendering and behavior together narrows the code involved in many changes.
  • Focused tests: Components can be exercised through their visible output and interactions at a defined boundary.

2. Declarative rendering and reconciliation

Components return React elements that describe the desired UI. React reconciles a new description with the previous one and lets the renderer commit the required host changes. This gives applications a predictable declarative update model and avoids replacing unchanged DOM nodes. It has its own rendering and diffing costs, so it is not a guarantee that React is faster than carefully written imperative DOM code.

3. Large and active community

React has a mature ecosystem, which affects practical framework and library choices:

  • Maintained documentation and learning material: The official documentation covers both fundamentals and current APIs.
  • Third-party libraries and tools: Established options exist for routing, data fetching, testing, forms, and accessible UI primitives.
  • A large hiring and support base: Many teams already have React experience, examples, and production knowledge to draw from.

4. One-way data binding

React uses a unidirectional data flow: data is passed from parent components down to children via props, and child components request updates through callbacks rather than mutating parent state directly. This gives each value an explicit owner and update path.

5. Hooks and function components

Modern React is built around function components and Hooks. Hooks such as useState, useEffect, useReducer, useContext, and useRef provide state, synchronization, and context access without classes. Custom Hooks extract related stateful logic for reuse across components.

6. Learn once, write anywhere

React's component and state model also applies to React Native. Web and native applications can often share business logic, data hooks, and design concepts, but their host components and platform-specific interaction code usually differ.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

Which statements describe React itself rather than guarantees of a particular framework? Select all that apply.

What is the difference between React Node, React Element, and a React Component?

Topics
React

TL;DR

A React Node is a value React can render, including an element, string, number, bigint, iterable of nodes, portal, or an empty value such as null or a boolean. React 19's type also supports promises that resolve to renderable nodes in supported rendering environments. A React Element is the immutable object produced by JSX or createElement that describes what to render. A React Component is a function or class React uses as an element type. Components produce nodes; elements are descriptions; nodes are the broader set of renderable values.


React node

A React Node is anything that can appear in a JSX child position. The set includes:

  • React Elements (e.g. <div />, <MyComponent />)
  • Strings, numbers, and bigints (rendered as text)
  • Arrays and other iterables of React Nodes (arrays are commonly produced by .map(...))
  • Fragments (<>...</> or <React.Fragment>)
  • Portals (created with createPortal)
  • Promises of React Nodes in rendering environments that support suspending on them
  • null, undefined, false, and true — these are valid nodes that render nothing (they are skipped, not converted to text)
const stringNode = 'Hello, world!';
const numberNode = 123;
const arrayNode = [<li key="a">a</li>, <li key="b">b</li>];
const fragmentNode = (
<>
<span>one</span>
<span>two</span>
</>
);
const nothingNode = null; // also: undefined, false, true — render nothing
const elementNode = <div>Hello, world!</div>;

This is why condition && <Thing /> works: when condition is false, the child is just a node that renders nothing.

React element

A React Element is an immutable, plain JavaScript object that describes what should appear on screen. It includes a type (a string for host elements like 'div', or a component reference), props (including children), and a key. JSX is sugar for React.createElement (or, with the modern JSX transform, react/jsx-runtime's jsx function), which produces these objects.

const element = <div className="greeting">Hello, world!</div>;
// Roughly equivalent to:
const sameElement = React.createElement(
'div',
{ className: 'greeting' },
'Hello, world!',
);

React elements should be treated as opaque and immutable rather than inspected or modified directly. They are lightweight, short-lived descriptions that React reconciles with the previous output before the renderer commits host changes.

React component

A React Component is a function or class that React can use as an element type. It accepts props and returns React Nodes that describe the UI; an async Server Component may return a promise of a node. Component names are conventionally PascalCase so JSX can distinguish them from host elements (<button> is a DOM element; <Button> is a component).

  • Function components are the standard form. They are plain functions of props that return a node:

    function Welcome({ name }) {
    return <h1>Hello, {name}</h1>;
    }

    Hooks (useState, useEffect, useMemo, etc.) cover what used to require a class.

  • Class components remain supported, but React recommends function components for new code. Legacy lifecycles such as componentWillMount, componentWillReceiveProps, and componentWillUpdate are deprecated, and Hooks are available only in functions.

    class Welcome extends React.Component {
    render() {
    return <h1>Hello, {this.props.name}</h1>;
    }
    }

The mental model: a component is a recipe, an element is a single dish made from that recipe, and a node is anything React is willing to plate up — including "nothing."

The relationships are easiest to see by following one component invocation from JSX to renderable output:

Components, elements, and nodes

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which mapping is correct?

What is JSX and how does it work?

Topics
React

TL;DR

JSX is a syntax extension for JavaScript that lets you describe UI with HTML-like markup inside JavaScript. A build tool transforms it into ordinary function calls before the browser runs it. With the modern automatic runtime, <div>Hello, world!</div> becomes a call to a helper from react/jsx-runtime; the older classic transform used React.createElement.


What is JSX and how does it work?

JSX is syntax that a build tool transforms into calls producing React element descriptions; browsers do not execute JSX directly.

What is JSX?

JSX is a syntax extension for JavaScript that lets you describe UI trees with an HTML-like syntax. Although it was popularized by React, JSX itself is a separate spec and is also used by other libraries such as Preact and Solid. TypeScript supports it natively in .tsx files.

How does JSX work?

JSX is not valid JavaScript by itself. A compiler — typically Babel or the bundler's built-in transform (SWC, esbuild, Oxc) — converts JSX into ordinary JavaScript function calls before the browser sees it.

JSX syntax

JSX allows you to write HTML-like tags directly in your JavaScript code. For example:

const element = <h1>Hello, world!</h1>;

Transformation process

Since React 17 (2020), the default transform is the automatic JSX runtime. Instead of compiling to React.createElement, the compiler imports jsx / jsxs helpers from react/jsx-runtime and emits calls to those. A consequence is that you no longer need to import React from 'react' just to use JSX:

// Source
const element = <h1>Hello, world!</h1>;
// Output with the automatic runtime (conceptually)
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h1', { children: 'Hello, world!' });

The older "classic" transform compiled the same JSX to React.createElement('h1', null, 'Hello, world!') and required React to be in scope. The classic form is still useful as a mental model for what JSX desugars to, but new projects should use the automatic runtime.

Regardless of which transform is configured, JSX becomes ordinary JavaScript before React receives an element description:

JSX transformation and rendering pipeline

Embedding expressions

You can embed JavaScript expressions inside JSX using curly braces {}. For example:

const name = 'John';
const element = <h1>Hello, {name}!</h1>;

Attributes in JSX

You can use quotes to specify string literals as attributes and curly braces to embed JavaScript expressions. For example:

const element = <img src={user.avatarUrl} alt="User Avatar" />;

Because JSX attributes compile to JavaScript object keys, a few HTML attribute names are renamed to avoid clashing with reserved words or to follow JS camelCase conventions:

  • class becomes className
  • for becomes htmlFor
  • Event handlers are camelCased: onclick becomes onClick, onchange becomes onChange
  • Most other DOM properties (tabIndex, readOnly, maxLength, etc.) use camelCase

Fragments

To return multiple elements without an extra wrapper DOM node, use a fragment. The shorthand syntax is <>...</>:

function List() {
return (
<>
<li>One</li>
<li>Two</li>
</>
);
}

The longer form <Fragment key={id}>...</Fragment> is needed when you must pass a key.

JSX is an expression

After compilation, JSX expressions become regular JavaScript function calls and evaluate to JavaScript objects. This means you can use JSX inside if statements, assign it to variables, and pass it as a prop or argument.

JSX prevents injection attacks

By default, React DOM escapes any values embedded in JSX with {} before rendering them, which neutralizes the most common XSS vector — injecting markup via untrusted strings:

const userInput = '<img src=x onerror="alert(1)" />';
const safe = <div>{userInput}</div>; // rendered as text, not as HTML

This protection is not absolute. Two notable escape hatches still bypass escaping and can introduce XSS if fed untrusted data:

  • dangerouslySetInnerHTML={{ __html: ... }} injects raw HTML into the DOM.
  • React 19 blocks javascript: URLs in URL-valued attributes such as href and src. You should still validate attacker-controlled URLs against the schemes and destinations your application permits because other schemes, redirects, and resource types can carry their own risks.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

What happens to JSX before a typical browser executes the application?

What is the difference between state and props in React?

Topics
React

TL;DR

State is data a component owns and can update over time; props are data a component receives from its parent and must treat as read-only. A state update schedules the owning component to render again unless React can apply a same-value bailout; descendants render by default but may also bail out. New props arrive when the parent renders new element descriptions. Together they implement React's one-way data flow: state lives at the lowest common ancestor that needs it, flows down as props, and changes flow back up via callbacks passed as props.


What is the difference between state and props in React?

State is component-owned memory, while props are read-only inputs supplied by the component's parent.

State

State is data a component owns and can change over time, usually in response to user interaction, network responses, or timers. When state changes, React schedules a re-render of that component so the UI reflects the new value.

  • State is local: a parent cannot read a child's state directly.
  • In function components, state is declared with the useState hook (or useReducer for more complex transitions).
  • Setters queue state for a subsequent render and React batches updates where possible. The current handler still sees the state snapshot from the render that created it.
  • The term updater function specifically refers to the setX(prev => next) form passed to a setter — not the setter itself. Use it whenever the next value depends on the previous one, so batched updates compose correctly.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// This works for one click, but repeated queued calls would reuse the same
// render snapshot: const increment = () => setCount(count + 1);
// Right: the updater function gets the latest value
const increment = () => setCount((prev) => prev + 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button
onClick={() => {
// Both updates apply; final count goes up by 2
increment();
increment();
}}>
Increment twice
</button>
</div>
);
}

Props

Props (short for "properties") are the inputs a parent passes to a child. From the child's perspective they are read-only — you must not assign to them. From the system's perspective they are not "immutable" in any deep sense; the parent simply re-renders with a new value, and the child receives the new props on its next render.

function Parent() {
const [name, setName] = useState('World');
return (
<>
<input value={name} onChange={(event) => setName(event.target.value)} />
<Greeting message={`Hello, ${name}!`} />
</>
);
}
function Greeting({ message }) {
// Read-only here. Mutating `message` would be a bug.
return <p>{message}</p>;
}

Props can carry data, JSX (children), and callbacks. Callback props are how children communicate upward — the child invokes the function, the parent updates its state, and new props flow down on the next render.

One-way data flow, lifted state, and derived values

These three ideas tie state and props together:

  • One-way data flow. Data moves down the tree as props. To affect a parent, a child calls a function the parent passed in. There is no two-way binding.
  • Lifting state up. When two siblings need the same data, move the useState to their nearest common ancestor and pass the value (and a setter) down as props. This keeps a single source of truth.
  • Derived state vs state. Anything you can compute from props or existing state during render should be computed, not stored. Storing a derived value duplicates the source of truth and creates sync bugs; just calculate it in the render body (and reach for useMemo only if the computation is expensive).
function Cart({ items }) {
// Derived from props — do NOT put this in useState
const total = items.reduce((sum, item) => sum + item.price, 0);
return <p>Total: {total}</p>;
}

Key differences

The following table compares ownership, updates, and rendering behavior:

StateProps
Owned byThe component itselfThe parent
Mutable byThe component, through its setterThe child must treat props as read-only; the parent may pass a new value
Triggers re-render ofThe owning componentThe receiving component, when the parent passes new values
Typical useInternal, changing dataConfiguration, data, and callbacks passed in

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which statements correctly distinguish state from props? Select all that apply.

What is the purpose of the `key` prop in React?

Topics
React

TL;DR

The key prop tells React how to identify each child in a list across renders so it can match the right component instance to the right data, preserve its state, and reorder DOM nodes correctly. A key only needs to be unique among siblings, not globally. Changing a component's key is also the idiomatic way to reset its state — React unmounts the old instance and mounts a fresh one.

<ul>
{items.map((item) => (
<ListItem key={item.id} value={item.value} />
))}
</ul>

What is the purpose of the key prop in React?

Keys give sibling elements stable identity so reconciliation can preserve or reset the correct component state and DOM.

Introduction

The key prop is a special attribute you need to include when creating lists of elements in React. It is crucial for helping React identify which items have changed, been added, or removed, thereby optimizing the rendering process.

Why key is important

Stable identity affects correctness before it affects rendering efficiency:

  1. Correct identity during reconciliation: When React diffs a list, the key is how it decides which previous element corresponds to which new one. Without keys (or with bad keys) React can still render the list, but it will associate the wrong component instance with the wrong data — which means the wrong internal state, refs, and DOM nodes get reused. The bigger risk is incorrect state association, not raw DOM operation count.
  2. Efficient reordering: With stable keys, React can move existing DOM nodes instead of unmounting and remounting them when items are reordered.
  3. Explicit state reset: Changing a component's key deliberately is the idiomatic way to force React to unmount the old instance and mount a brand-new one (see Resetting state with a key below).

How to use the key prop

When rendering a list of elements, you should provide a unique key for each element. This key should be stable, meaning it should not change between renders. Typically, you can use a unique identifier from your data, such as an id.

const items = [
{ id: 1, value: 'Item 1' },
{ id: 2, value: 'Item 2' },
{ id: 3, value: 'Item 3' },
];
function ItemList() {
return (
<ul>
{items.map((item) => (
<ListItem key={item.id} value={item.value} />
))}
</ul>
);
}
function ListItem({ value }) {
return <li>{value}</li>;
}

Rules for keys

A useful key must remain tied to the same logical sibling across renders:

  • Unique among siblings, not globally: Keys only need to be unique within a single map() / array. Two unrelated lists can happily both contain a key="1".
  • Stable across renders: The same piece of data should get the same key every render. Avoid Math.random() or Date.now().
  • Keys are not passed to the child: key is consumed by React itself. If your child component also needs the id, pass it as a separate prop.

Keys on Fragments

When you render a list of fragments, you cannot use the <>...</> shorthand because it does not accept props. Use the long-form <Fragment key={...}> instead:

import { Fragment } from 'react';
function Glossary({ entries }) {
return (
<dl>
{entries.map((entry) => (
<Fragment key={entry.term}>
<dt>{entry.term}</dt>
<dd>{entry.definition}</dd>
</Fragment>
))}
</dl>
);
}

Common mistakes

Most key bugs come from deriving identity from render position or generating a new identity during render:

  1. Using array index as key for dynamic lists: If the list can be reordered, filtered, or have items inserted in the middle, using the index causes React to associate the wrong state with the wrong item — a classic cause of "the input I typed into moved to the wrong row" bugs. Index-as-key is acceptable only for static, append-only lists that never reorder.
  2. Non-unique keys: Duplicate keys among siblings trigger a warning and cause React to mis-match items during reconciliation.
  3. Generating a new key every render: key={Math.random()} forces every item to remount on every render — killing performance and wiping state.
function BadList({ items }) {
return (
<ul>
{items.map((item, index) => (
<ListItem key={index} value={item.value} />
))}
</ul>
);
}
function GoodList({ items }) {
return (
<ul>
{items.map((item) => (
<ListItem key={item.id} value={item.value} />
))}
</ul>
);
}

Resetting state with a key

Because React treats a component with a new key as a different instance, changing the key unmounts the old component and mounts a new one — resetting all of its state, refs, and effects. This is the idiomatic way to reset a form or other stateful subtree when some identifier changes:

function ProfileEditor({ userId }) {
// When userId changes, the Form unmounts and a fresh one mounts with
// pristine local state — no manual reset logic needed.
return <Form key={userId} userId={userId} />;
}

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which statements about React keys are correct? Select all that apply.

What is the consequence of using array indices as the value for `key`s in React?

Topics
React

TL;DR

Using array indices as keys causes React to reconcile the list incorrectly when items are reordered, inserted, or removed. Because the key identifies a position rather than an item, React reuses the wrong component instances — leaving stale local state, focus, and DOM attached to the wrong rows. The fix is to use a stable, unique identifier from the data (e.g. item.id). Index keys are only safe when the list is static and never reordered, filtered, or prepended to.


Consequence of using array indices as the value for keys in React

An index identifies a list position rather than the logical item occupying it, which breaks identity when the list changes before that position.

Incorrect reconciliation, not just "slow renders"

The real problem is correctness, not performance. React's key is the identity React uses to match an element across renders. When the key is the array index, the identity tracks the slot in the array rather than the item in the slot. After a reorder, insert, or delete, React still matches key={0} to key={0}, so it:

  • Keeps the same component instance mounted for what is now a different item.
  • Preserves local useState, refs, focus, scroll position, and uncontrolled input values on the wrong row.
  • Skips bailouts it would otherwise hit, because props for the reused instance changed.

In other words, the bug is "state and DOM tied to the wrong items." Re-render cost is a secondary consequence.

The classic input/checkbox state-bleed bug

This is the canonical demonstration. Each row owns an uncontrolled <input>; the user types into the second one, then the list is reordered or the first item is deleted.

function TodoList({ todos, onDelete }) {
return (
<ul>
{todos.map((todo, index) => (
// Bug: key is the index
<li key={index}>
<input type="checkbox" defaultChecked={todo.done} />
<input type="text" defaultValue={todo.title} />
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}

If the user checks the second row and then deletes the first row, React reuses the old <li key={0}> for the item that moved into index 0. That item therefore inherits the first row's uncontrolled DOM state. The checked DOM from the old <li key={1}> either moves to the item now at index 1 or disappears if no row remains there. Switching to key={todo.id} fixes the mismatch because React removes only the deleted item's row and preserves each surviving item's DOM with that item.

The two key strategies give React different answers about which row survived the deletion:

Item identity after deleting the first row

When index keys are acceptable

Index keys are fine when all of the following hold:

  • The list is static (never reordered or filtered).
  • Items are never inserted or removed from anywhere except the end.
  • Each item has no per-row state, focus, or uncontrolled input that could leak.

A render-once nav menu built from a constant array fits. A live, editable, sortable, or paginated list does not.

Omitting the key is even worse

If you leave key off entirely, React falls back to using the index and prints a Warning: Each child in a list should have a unique "key" prop in development. Suppressing the warning by passing key={index} does not fix the underlying issue — it just hides the message.

Better approach

Use a stable id that comes from the data, not from the render position.

const items = [
{ id: 'a1', name: 'Item 1' },
{ id: 'b2', name: 'Item 2' },
{ id: 'c3', name: 'Item 3' },
];
const List = () => (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);

If the data has no natural id, generate one when the item is created (e.g. crypto.randomUUID()) and store it on the item — do not generate a fresh id inside map, because it would change every render and defeat reconciliation entirely.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A reorderable list of editable rows uses each array index as its key. The first two records swap positions. Which failure is most likely?

What is the difference between controlled and uncontrolled React components?

Topics
React

TL;DR

A controlled component drives a form input from React state — you pass value/checked plus an onChange handler, and React state is the single source of truth. An uncontrolled component lets the DOM keep the value; you read it via a ref (or on submit) and seed the initial value with defaultValue/defaultChecked. Controlled inputs are the right default when you need validation, conditional UI, or to derive other state from the value. Uncontrolled inputs are simpler for write-once forms and for <input type="file">, which is always uncontrolled. React 19 also added first-class form support via the form action prop, useFormStatus, and useActionState, which often removes the need for per-field controlled state.


What is the difference between controlled and uncontrolled React components?

The distinction is which system owns the current form value: React state for a controlled input or the DOM for an uncontrolled input.

Controlled components

A controlled input passes both value (or checked) and onChange to the element. React state holds the truth; every keystroke flows through a setter.

import { useState } from 'react';
function ControlledForm() {
const [name, setName] = useState('');
function handleSubmit(event) {
event.preventDefault();
alert('A name was submitted: ' + name);
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
/>
</label>
<input type="submit" value="Submit" />
</form>
);
}

Uncontrolled components

An uncontrolled input keeps its value in the DOM. Seed the initial value with defaultValue (or defaultChecked for checkboxes/radios), and read the current value through a ref when you need it.

import { useRef } from 'react';
function UncontrolledForm() {
const inputRef = useRef(null);
function handleSubmit(event) {
event.preventDefault();
alert('A name was submitted: ' + inputRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" defaultValue="" ref={inputRef} />
</label>
<input type="submit" value="Submit" />
</form>
);
}

defaultValue is only consulted on the initial render — changing it later does not update the DOM. Passing value without an onChange handler makes the input read-only and warns in development unless readOnly is intentional. The reverse is valid: an input with onChange but no value remains uncontrolled. Pick one mode per field and do not switch modes during the input's lifetime.

<input type="file"> is always uncontrolled

File inputs cannot be controlled — their value is read-only for security reasons (a page must not be able to set the user's chosen file). Always read files via a ref or from the change/submit event, even in an otherwise controlled form.

function FileForm() {
const fileRef = useRef(null);
function handleSubmit(event) {
event.preventDefault();
const file = fileRef.current.files[0];
// upload file...
}
return (
<form onSubmit={handleSubmit}>
<input type="file" ref={fileRef} />
<button type="submit">Upload</button>
</form>
);
}

React 19 form actions

React 19 made <form action={...}> a first-class way to handle submissions without per-field controlled state. The action receives a FormData object, and useFormStatus / useActionState expose pending and result state.

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>;
}
async function saveName(_previousState, formData) {
const name = String(formData.get('name') ?? '');
const response = await fetch('/api/profile', {
method: 'POST',
body: formData,
});
if (!response.ok) return { ok: false, error: 'Could not save name.' };
return { ok: true, name };
}
function NameForm() {
const [state, formAction] = useActionState(saveName, null);
return (
<form action={formAction}>
<input name="name" defaultValue={state?.name ?? ''} />
<SubmitButton />
{state?.error && <p role="alert">{state.error}</p>}
</form>
);
}

This pattern uses uncontrolled inputs (defaultValue plus name) and reads them out of FormData in the action — often the simplest choice for plain submit-style forms.

Key differences

The ownership choice changes how values update and which use cases each style fits.

State management

Controlled and uncontrolled inputs store their current value in different places:

  • Controlled: React state owns the value; the DOM mirrors it.
  • Uncontrolled: The DOM owns the value; React reads it on demand.
Data flow

Both styles can report changes, but only the controlled style feeds every rendered value back through React state:

  • Controlled: state -> value -> input, and onChange -> setState.
  • Uncontrolled: defaultValue -> input, then ref.current.value (or FormData) when read.

The controlled loop continuously routes the value through React, while the uncontrolled path leaves ongoing ownership in the DOM:

Controlled and uncontrolled input data flow
When to use which

Choose according to whether application logic must observe or transform each value change:

  • Controlled: validation as the user types, conditional disabling, formatting, deriving other state, or anything that needs to react to every keystroke.
  • Uncontrolled: simple submit-once forms, integration with non-React code, file inputs, and forms built around React 19 actions.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

Which statements about controlled and uncontrolled form inputs are correct? Select all that apply.

What are some pitfalls about using context in React?

Topics
React

TL;DR

Context in React is convenient but easy to misuse. The biggest pitfalls are passing a fresh object or array as the provider value on every render, assuming memo or React Compiler will stop a context subscription update (they won't), and putting frequently changing, unrelated data into one context. Split independent values into focused providers, stabilize object values when appropriate, and consider a selector-based state library when consumers need different slices of rapidly changing state.


Pitfalls of using context in React

Context is convenient for distribution, but its broadcast update model and implicit dependencies can become costly when a value changes frequently or grows too broad.

Unnecessary re-renders from an unstable provider value

When a context value changes by reference, every component that reads that context re-renders — even if it only uses a field that hasn't actually changed. The most common cause is constructing a new object inline as the provider's value, which makes a fresh reference on every parent render:

// Pitfall — `value` is a new object every render, so every consumer re-renders
function ParentComponent() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
return (
<MyContext.Provider value={{ user, setUser, theme, setTheme }}>
<ChildComponent />
</MyContext.Provider>
);
}

Fix it by memoizing the value. React Compiler may generate equivalent memoization when enabled, but Context still propagates every genuine value change:

function ParentComponent() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({ user, setUser, theme, setTheme }),
[user, theme],
);
return (
<MyContext.Provider value={value}>
<ChildComponent />
</MyContext.Provider>
);
}

Note: only consumers of the context re-render when the value changes — not "all components in the subtree." Components that don't call useContext/use(MyContext) are unaffected.

React.memo doesn't stop context-driven re-renders

A common surprise: wrapping a consumer in React.memo does not prevent re-renders triggered by a context value change. memo only skips re-renders caused by changing props. If the component reads a context whose value changed, it re-renders regardless. The fix is to make the context value stable (above) or split the context.

Putting too much unrelated state in one context

If you cram an entire app's state into a single context, every change to any slice re-renders every consumer. Split it into smaller, focused providers — for example, separate AuthContext, ThemeContext, and CartContext — so that a cart update doesn't re-render every theme consumer. You can also split read and write APIs into separate contexts so components that only need to dispatch don't re-render when state changes.

No built-in selectors

Unlike Redux's useSelector, React context has no built-in way to subscribe to a slice of the value. Any change to the value re-runs every consumer. Workarounds include:

  • Splitting the context into smaller pieces (preferred).
  • The community use-context-selector library, which adds selector-based subscriptions.

Using context as a state manager

Context transports a value through a subtree; pairing it with useState or useReducer can be a reasonable state solution for a small application. Context does not add caching, middleware, devtools, or selector-based subscriptions by itself. Consider Redux Toolkit, Zustand, or Jotai when those capabilities solve a concrete client-state problem. Fetched data can be passed through context, but a framework data layer or a library such as TanStack Query, SWR, or RTK Query is usually a better fit when it needs caching, deduplication, invalidation, or background refetching.

Debugging difficulties

Because context updates can fan out across the tree, tracking down which provider caused a re-render can be hard, especially with nested providers. The React DevTools "Profiler" tab and "Why did this render?" highlighting help here, but it's still a good reason to keep providers small and focused.

React 19: use(Context) as an alternative to useContext

In React 19 you can read a context with the use API. Despite its name, use is not a Hook. Unlike useContext, it can be called inside conditionals and loops, but it has the same context subscription behavior:

import { use } from 'react';
function Profile() {
const user = use(UserContext);
return <div>{user.name}</div>;
}

useContext still works and is not going away — use(Context) is just more flexible.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

Which provider designs can cause avoidable context problems? Select all that apply.

What are the benefits of using hooks in React?

Topics
React

TL;DR

Hooks let function components use state, context, refs, effects, and other React features without classes. Custom Hooks compose reusable stateful logic without adding wrapper components. React 19 added Hooks such as useActionState and useOptimistic, while React DOM provides useFormStatus. React 19's similarly named use(resource) is an API rather than a Hook and follows different call-order rules.


Benefits of using hooks in React

Hooks let function components compose stateful behavior while keeping related setup, updates, and cleanup together.

Why hooks were introduced

Before hooks (React 16.8, Feb 2019), the main ways to share stateful logic between components were higher-order components (HOCs) and render props. Repeated use of either pattern could produce deeply nested wrapper trees and obscure data flow in React DevTools. Class components also required this handling and often split one synchronization concern across componentDidMount, componentDidUpdate, and componentWillUnmount.

Hooks let components compose stateful logic through functions without adding wrapper components or relying on class lifecycle methods.

Reusable logic via custom hooks

Custom hooks let you extract a related combination of state, Effects, and other hooks into a function that components can call. Unlike an HOC, each custom Hook adds no wrapper component to the rendered tree.

import { useSyncExternalStore } from 'react';
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true;
}
function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
function StatusBar() {
const isOnline = useOnlineStatus();
return <div>{isOnline ? 'Online' : 'Offline'}</div>;
}

This version uses useSyncExternalStore because online status is a browser value that changes outside React. The server snapshot also avoids reading navigator during server rendering and gives hydration a deterministic initial value.

Simplified state management

useState adds local state to any function component without converting it to a class:

import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((value) => value + 1)}>{count}</button>
);
}

For more complex state transitions, useReducer gives you a Redux-style reducer locally to a component or feature.

Side effects without lifecycle methods

useEffect replaces componentDidMount, componentDidUpdate, and componentWillUnmount with a single API where setup and cleanup live next to each other instead of being scattered across three methods:

import { useEffect } from 'react';
import { createConnection } from './chat-api';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <h1>Room: {roomId}</h1>;
}

No more this

Function components and hooks have no this, so there's nothing to bind, no .bind(this) in constructors, and no surprises about what this refers to in a callback.

React 19 Hooks and APIs unlock new patterns

React 19 adds several Hooks and related APIs that solve problems custom Hooks alone could not:

  • use(promise) — read a promise (or context) inside a component, integrating with Suspense for loading states.
  • useActionState — manage a form action's state (pending, error, result) with a single hook.
  • useFormStatus — read the pending state of the nearest parent <form> from a child, e.g. to disable a submit button.
  • useOptimistic — show an optimistic UI update while a mutation is in flight, automatically reverting on failure.

React Compiler 1.0 can additionally reduce manual useMemo/useCallback work in compatible code when the optional build-time optimizer is enabled.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

Two function components need the same subscription lifecycle but render completely different markup. What is the main benefit of extracting a custom Hook?

What are the rules of React hooks?

Topics
React

TL;DR

React hooks have a few essential rules to ensure they work correctly. Call hooks at the top level of a function component or custom hook — never inside loops, conditions, nested functions, or after an early return. The use API is the exception: it is not a Hook and may be called conditionally or in loops, but it must still run inside a component or Hook and cannot be wrapped in try/catch. Lean on eslint-plugin-react-hooks to enforce these rules.


What are the rules of React hooks?

The Rules of Hooks preserve a consistent call order so React can associate each Hook call with the correct component state across renders.

Always call hooks at the top level

Hooks must be called in the same order on every render. That means you cannot call them inside loops, conditions, nested functions, or after an early return. React identifies which useState/useEffect/etc. call corresponds to which piece of state purely by call order — break the order and React's internal bookkeeping desyncs.

import { useState } from 'react';
function Counter({ enabled }) {
const [count, setCount] = useState(0);
if (!enabled) return null;
return (
<button onClick={() => setCount((value) => value + 1)}>{count}</button>
);
}
// Incorrect — hook inside an `if`
function ConditionalCounter({ enabled }) {
if (enabled) {
const [count, setCount] = useState(0); // hook order changes between renders
return <div>{count}</div>;
}
return null;
}
// Incorrect — hook after an early return
function SelectableList({ items }) {
if (items.length === 0) return null;
const [selected, setSelected] = useState(null); // skipped on the early-return path
return <List items={items} selected={selected} onSelect={setSelected} />;
}

To fix the early-return case, move the hook above the conditional:

import { useState } from 'react';
function SelectableList({ items }) {
const [selected, setSelected] = useState(null);
if (items.length === 0) return null;
return <List items={items} selected={selected} onSelect={setSelected} />;
}

Stable call order lets React match each Hook to the same internal slot on every render; conditionally skipping one shifts every later match:

Why Hook call order must stay stable

The special case: use

Despite its name, React's use(resource) API is not a Hook. You may call it inside conditions and loops because it does not store state by call order:

import { use } from 'react';
function Comments({ shouldLoad, commentsPromise }) {
if (shouldLoad) {
const comments = use(commentsPromise);
return <CommentList comments={comments} />;
}
return null;
}

use still must be called from a component or custom Hook. It also cannot be wrapped in try/catch; use an error boundary to handle a rejected promise.

Only call hooks from React functions

Hooks can only be called from:

  1. React function components.
  2. Other custom hooks (which by convention must have a name starting with use).

Calling a hook from a regular utility function, a class component, or an event handler is not allowed.

import { useState } from 'react';
// Correct — function component
function MyComponent() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
// Correct — custom hook (name starts with `use`)
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = () => setCount((c) => c + 1);
return { count, increment };
}
// Incorrect — plain function, not a component or hook
function regularFunction() {
const [count, setCount] = useState(0); // violates the rules of hooks
}

The use prefix isn't cosmetic — it identifies a function as a custom Hook and lets the linter enforce call-order rules at its call sites. If a function that calls Hooks is instead named getCounter, the linter reports that Hooks are being called from a function that is neither a component nor a custom Hook.

Use eslint-plugin-react-hooks

The eslint-plugin-react-hooks package automates enforcement of these rules. Its two foundational rules are react-hooks/rules-of-hooks (call order and valid call sites) and react-hooks/exhaustive-deps (reactive dependencies for Effects and memoization Hooks). The recommended preset also enables additional Rules of React and React Compiler diagnostics.

npm install eslint-plugin-react-hooks --save-dev

For a legacy .eslintrc config:

{
"extends": ["plugin:react-hooks/recommended"]
}

For ESLint's flat config (eslint.config.js), use the plugin's flat recommended preset:

import { defineConfig } from 'eslint/config';
import reactHooks from 'eslint-plugin-react-hooks';
export default defineConfig([reactHooks.configs.flat.recommended]);

The package also provides Compiler-powered lint rules. Keep it current when using newer APIs such as useEffectEvent so dependency suggestions follow their special semantics.

A note on the React Compiler

React Compiler 1.0 is a stable, optional build-time tool. When it is enabled, it can reduce the need for hand-written useMemo, useCallback, and memo. It does not change the rules above: ordinary Hooks must still be called unconditionally at the top level, and use retains its separate rules. The Compiler relies on code following the Rules of React and skips optimizations it cannot safely apply.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which calls follow the Rules of Hooks? Select all that apply.

What is the difference between `useEffect` and `useLayoutEffect` in React?

Topics
React

TL;DR

Both hooks run side effects after render, but they differ in when they fire relative to paint:

  • useEffect runs after React commits. For effects not caused by an interaction, React generally lets the browser paint first; interaction-driven effects may run before paint. Use it for synchronizing with external systems when the work does not need to block paint.
  • useLayoutEffect runs synchronously during the commit phase, after DOM mutations but before the browser paints. It blocks paint, so use it only when you need to measure the DOM and write to it in the same frame to avoid a visual flicker.

Both accept a dependency array with the same semantics. In development Strict Mode, React performs an extra setup-and-cleanup cycle before the real setup. Neither effect runs during server rendering; useLayoutEffect is especially unsuitable there because the server has no layout to measure.

Code example:

import { useEffect, useLayoutEffect, useRef } from 'react';
function Example() {
const ref = useRef(null);
useEffect(() => {
console.log('useEffect: runs after paint');
}, []);
useLayoutEffect(() => {
console.log('useLayoutEffect: runs before paint');
console.log('Element width:', ref.current.offsetWidth);
}, []);
return <div ref={ref}>Hello</div>;
}

What is useEffect?

useEffect schedules synchronization work after React commits changes to the DOM. If the effect was not caused by an interaction, React generally lets the browser paint first. For interaction-driven effects, React may run it before paint so the event system can observe the result. If timing relative to paint is essential, use the appropriate browser scheduling API or useLayoutEffect rather than relying on useEffect always running afterward.

  • It is the right default for data fetching, subscriptions, event listeners, and logging.
  • The dependency array controls when React re-synchronizes: [a, b] means after the initial commit and after commits where a or b changed by Object.is; [] means after mounting; omitting the array means after every commit.
  • In development Strict Mode, React runs one extra setup-and-cleanup cycle before the real setup. This surfaces missing cleanup logic; it is not a production behavior.

Code example

This Effect synchronizes with an external system after the component commits:

import { useEffect } from 'react';
function Example() {
useEffect(() => {
console.log('Mounted');
return () => console.log('Cleanup on unmount');
}, []); // [] deps: cleanup runs on unmount (plus the Strict Mode stress test in development)
return <div>Hello, World!</div>;
}

In production, cleanup for an Effect with [] dependencies runs when the component unmounts. In development Strict Mode, React also performs the extra setup-and-cleanup stress test described above. With non-empty dependencies such as [userId], cleanup runs before the next setup when userId changes and again on unmount.

Common use cases

Use useEffect for synchronization that does not need to block the browser's next paint:

  • Fetching data from an API
  • Setting up subscriptions (e.g., WebSocket connections)
  • Logging or analytics tracking
  • Adding and removing event listeners that do not affect layout

What is useLayoutEffect?

useLayoutEffect runs synchronously during the commit phase, after React has written to the DOM but before the browser paints. Because it blocks paint, anything you do inside it delays the first visible frame — but in return you can measure the just-committed DOM and make adjustments atomically, with no flicker.

  • Use it when you need to read layout (e.g. getBoundingClientRect, offsetWidth) and then synchronously set state or style so the user never sees the "wrong" frame.
  • Keep the body cheap — expensive work here stalls paint.
  • Effects do not run during server rendering, and useLayoutEffect cannot contribute to server HTML because there is no layout to measure. Prefer useEffect when the first painted frame can be corrected later. If the content fundamentally depends on layout, render that content only after hydration. Libraries sometimes expose a useIsomorphicLayoutEffect alias that selects useEffect on the server and useLayoutEffect in the browser.

Code example

This layout Effect measures committed DOM and updates state before the browser paints the result:

import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip({ children }) {
const ref = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
// Measure and commit the height before the user sees a frame
setHeight(ref.current.getBoundingClientRect().height);
}, []);
return (
<div ref={ref} style={{ marginTop: -height }}>
{children}
</div>
);
}

Common use cases

Reserve useLayoutEffect for layout reads or writes that must complete in the same frame:

  • Measuring a DOM node and writing back state/style in the same frame
  • Positioning tooltips, popovers, or floating elements based on layout
  • Fixing flicker caused by a measure-then-correct pattern

useInsertionEffect

useInsertionEffect runs before layout effects so CSS-in-JS libraries can inject <style> tags before layout is read. It may run before or after the DOM itself is updated, refs are not attached yet, and it cannot update state. Application code should almost never use it; reach for useEffect or useLayoutEffect first.

Dependency arrays and the exhaustive-deps lint rule

Both hooks take a dependency array with the same rules. React compares each entry to the previous render with Object.is and re-runs the effect (after running the previous cleanup) when any of them changed. The react-hooks/exhaustive-deps ESLint rule flags missing dependencies and is considered required in most React codebases — silencing it usually hides a stale-closure bug. If a value would make the effect re-run too often, the fix is usually to move it inside the effect, memoize it, or convert it to a ref, not to omit it.

Key differences between useEffect and useLayoutEffect

The APIs share the same dependency model but run at different points relative to browser paint.

Timing

The timing distinction determines whether an Effect can delay visible output:

  • useEffect: Runs after commit and generally after paint for non-interaction effects, but may run before paint when caused by an interaction.
  • useLayoutEffect: Fires synchronously during commit, before the browser paints.

The usual non-interaction timeline puts layout work directly in the commit path and ordinary Effect work after the browser has painted:

Effect timing relative to browser paint

Blocking behavior

Layout Effects run synchronously in the commit path, while ordinary Effects generally allow paint first:

  • useEffect: Generally does not block paint for non-interaction Effects. Interaction-triggered Effects can run before paint when React needs the result to be observable to the event system.
  • useLayoutEffect: Blocks paint. The browser cannot repaint until the effect (and any resulting state update) has settled.

SSR behavior

Neither Effect runs during server rendering, but only the layout variant needs special care because its purpose depends on layout before paint:

  • useEffect: Skipped on the server (client runs it after hydration).
  • useLayoutEffect: Does not run on the server; use useEffect when possible or render layout-dependent content only after hydration so the server and initial client output still match.

Use case examples

Choose the least blocking API that still satisfies the synchronization requirement:

  • useEffect: fetching data, subscriptions, logging, most event listeners.
  • useLayoutEffect: measuring and adjusting the DOM in the same frame to prevent flicker.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

A tooltip must measure its rendered height and synchronously adjust position before the user sees the first frame. Which Hook is appropriate for that narrow synchronization?

What is the purpose of callback function argument format of `setState()` in React and when should it be used?

Topics
React

TL;DR

The callback (or updater function) form of setState — both this.setState(prev => ...) in classes and setX(prev => ...) with useState — guarantees that each update is computed from the latest queued state rather than the value captured in your closure. Use it whenever the next state depends on the previous state, especially when you call the setter more than once in the same event handler or when the update may run after an await/timeout/promise.

import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount((value) => value + 1);
setCount((value) => value + 1);
}
return <button onClick={handleClick}>{count}</button>;
}

Purpose of the updater function form of setState

The updater form calculates next state from React's latest queued state rather than from the render snapshot captured by a closure.

What it is

React's state setters — this.setState in class components and the setX returned by useState in function components — accept either a new value or an updater function. The updater receives the latest queued state (and, for class components, the latest props) and returns the next state.

The community calls this the updater function form (sometimes "functional updater"). Recognizing that name is helpful in interviews.

Why it exists: state is a snapshot and updates are queued

State setters do not change the state snapshot in the currently running code. React queues the update for a subsequent render. Since React 18, automatic batching applies to React events and many other callbacks, including promise callbacks, timers, and native event handlers.

That means by the time the queued update actually runs, the variable you captured from the previous render may be stale.

The motivating bug — reading state directly between successive calls

The classic mistake is calling the setter more than once based on the current state value:

import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// BUG: each call uses the same closed-over `count` (still 0 on this render).
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// After re-render, count is 1 — not 3.
};
return <button onClick={handleClick}>{count}</button>;
}

count is captured by the closure when the component rendered. All three calls compute 0 + 1, so React queues 1, 1, 1 and the final state is 1.

The same bug exists in classes — reading this.state.counter between setState calls returns the value from the last render, not the in-flight queued value.

The fix — pass an updater function

Passing a pure updater lets React apply each queued transformation in order:

import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
// Final count is 3 — each updater receives the result of the previous one.
};
return <button onClick={handleClick}>{count}</button>;
}

Each updater receives a snapshot of the latest queued state, not the value from your render closure.

When to use it

Use an updater whenever the next value is calculated from the previous value in the same state variable:

  • The next state depends on the previous state (counters, toggles, append-to-array, increment-a-map-entry).
  • You call the setter more than once in the same handler.
  • The update happens after an await, setTimeout, promise resolution, or subscription callback — an intervening update may have made the closed-over value stale.
  • Inside useEffect or useCallback when the state is read only to calculate its own next value — using the updater can remove that state value from the dependency list.

When you do not need it

If the new value does not depend on the previous state — setName('Alice'), setUser(response.data) — passing the value directly is fine and slightly more readable.

Class component equivalent

The same idea applies in class components, where the updater also receives props:

import { Component } from 'react';
class Counter extends Component {
state = { counter: 0 };
incrementCounter = () => {
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment,
}));
};
render() {
return (
<div>
<p>Counter: {this.state.counter}</p>
<button onClick={this.incrementCounter}>Increment</button>
</div>
);
}
}

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

The initial count is 4. What value is rendered after this handler?

function handleClick() {
setCount((value) => value + 2);
setCount((value) => value * 3);
}

What does the dependency array of `useEffect` affect?

Topics
React

TL;DR

The dependency array of useEffect controls when React re-synchronizes the effect. With no array, React runs setup after every commit. With [], it runs setup after the component mounts (development Strict Mode immediately performs an extra setup/cleanup cycle). With dependencies, it runs after mount and again whenever any dependency changes according to Object.is. Cleanup runs before re-synchronization and after unmounting.


What does the dependency array of useEffect affect?

The dependency list controls when React re-synchronizes an Effect with the reactive values used by its setup function.

Introduction to useEffect

The useEffect hook in React is used to perform side effects in functional components. These side effects can include data fetching, subscriptions, or manually changing the DOM. The useEffect hook takes two arguments: a function that contains the side effect logic and an optional dependency array.

Dependency array

The dependency array is the second argument to the useEffect hook. It is an array of values that the effect depends on. React uses this array to determine when to re-run the effect.

useEffect(() => {
// Side effect logic here
}, [dependency1, dependency2]);

How the dependency array affects useEffect

The three common dependency-list forms produce different setup and cleanup schedules:

  1. Empty dependency array ([]):

    • The effect runs once after the initial render and its cleanup runs once on unmount.
    • This is roughly analogous to componentDidMount plus componentWillUnmount, but with an important caveat: at a Strict Mode root in development, React performs an extra Effect setup-and-cleanup cycle before the real setup. This stress-tests cleanup without representing a second production mount.
    useEffect(() => {
    // This code runs after the initial render
    return () => {
    // Cleanup runs on unmount
    };
    }, []);
  2. Dependency array with variables:

    • The effect runs after the initial render and whenever any of the specified dependencies change.
    • React compares each dependency with its previous value using Object.is (reference equality, not a deep or shallow object comparison). Inline objects, arrays, or functions therefore change identity on every render unless memoized.
    useEffect(() => {
    // This code runs after the initial render and whenever dependency1 or dependency2 changes
    }, [dependency1, dependency2]);
  3. No dependency array:

    • The effect runs after every render.
    • This can lead to performance issues if the effect is expensive.
    useEffect(() => {
    // This code runs after every render
    });

The dependency-list form selects the schedule, while a changed dependency also determines when cleanup must precede the next setup:

How the dependency array schedules an Effect

Cleanup behavior on dependency change

When dependencies change, React first runs the previous effect's cleanup function (if any) before running the effect again with the new values. The same is true on unmount. This means dependency changes effectively perform a tear-down/set-up cycle, which matters for subscriptions, intervals, and event listeners.

useEffect(() => {
const subscription = source.subscribe(id);
return () => subscription.unsubscribe(); // runs before next effect or on unmount
}, [id]);

The react-hooks/exhaustive-deps lint rule

The official eslint-plugin-react-hooks ships an exhaustive-deps rule that warns when reactive values used inside an effect are missing from the dependency array. Treat its warnings as bugs — silencing the rule is almost always the wrong fix and a common source of stale closures.

Common pitfalls

Incorrect dependencies either leave an Effect synchronized with stale values or cause unnecessary repeated setup:

  1. Stale closures:

    • If you use state or props inside the effect without including them in the dependency array, you might end up with stale values.
    • Always include all state and props that the effect depends on in the dependency array.
    const [count, setCount] = useState(0);
    useEffect(() => {
    const handle = setInterval(() => {
    console.log(count); // This might log stale values if `count` is not in the dependency array
    }, 1000);
    return () => clearInterval(handle);
    }, [count]); // Ensure `count` is included in the dependency array

    React 19.2 introduced the stable useEffectEvent API for logic that is conceptually an event fired by an Effect. It can read the latest committed props and state without making the surrounding Effect re-synchronize:

    const onTick = useEffectEvent(() => {
    console.log(count); // always reads the latest count
    });
    useEffect(() => {
    const handle = setInterval(onTick, 1000);
    return () => clearInterval(handle);
    }, []); // effect does not need `count` in its deps

    Effect Events are not a general way to suppress dependencies: declare them in the same component or custom Hook as their Effect, call them only from Effects or other Effect Events, and do not include them in dependency arrays. eslint-plugin-react-hooks v6.1.1 or newer enforces these restrictions.

  2. Functions as dependencies:

    • Functions are recreated on every render, so including them in the dependency array can cause the effect to run more often than necessary.
    • Use useCallback to memoize functions if they need to be included in the dependency array.
    const handleClick = useCallback(() => {
    console.log('clicked');
    }, []);
    useEffect(() => {
    window.addEventListener('click', handleClick);
    return () => window.removeEventListener('click', handleClick);
    }, [handleClick]); // stable identity, so this effect does not re-subscribe each render

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

An Effect depends on roomId. The component commits with roomId="a", later commits with roomId="b", and then unmounts. Ignoring the extra development Strict Mode check, what is the order?

What is the `useRef` hook in React and when should it be used?

Topics
React

TL;DR

The useRef hook in React is used to create a mutable object that persists across renders. It can be used to access and manipulate DOM elements directly, store mutable values that do not cause re-renders when updated, and keep a reference to a value without triggering a re-render. For example, you can use useRef to focus an input element:

import { useEffect, useRef } from 'react';
function TextInputWithFocusButton() {
const inputEl = useRef(null);
useEffect(() => {
inputEl.current?.focus();
}, []);
return <input ref={inputEl} type="text" />;
}

What is the useRef hook in React and when should it be used?

useRef preserves a mutable container across renders for values that should not themselves trigger rendering.

Introduction to useRef

The useRef hook in React is a function that returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component. Updating ref.current does not trigger a re-render.

A few important rules:

  • Avoid reading or writing ref.current during rendering because it makes output depend on mutable data React does not track. The documented exception is predictable one-time initialization, such as filling a null ref with a lazily created object.
  • It is fine (and expected) to read or write ref.current inside event handlers, effects, or callbacks.

Key use cases for useRef

Refs primarily bridge to imperative host objects or retain non-rendered mutable values between component calls.

Accessing and manipulating DOM elements

One of the primary use cases for useRef is to directly access and manipulate DOM elements. This is particularly useful when you need to interact with the DOM in ways that are not easily achievable through React's declarative approach.

Example:

import { useEffect, useRef } from 'react';
function TextInputWithFocusButton() {
const inputEl = useRef(null);
useEffect(() => {
inputEl.current?.focus();
}, []);
return <input ref={inputEl} type="text" />;
}

In this example, useRef holds the input DOM node and the Effect focuses it after the node has been committed. Optional chaining handles the case where the ref is not attached.

Storing mutable values across renders

useRef can also be used to store any mutable value that should persist across renders without causing one. Common examples are interval/timeout IDs, the previous value of a prop or state, an instance of a non-React object (e.g. a chart or map controller), or a counter used inside event handlers.

import { useEffect, useRef, useState } from 'react';
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function start() {
if (intervalRef.current !== null) return;
intervalRef.current = window.setInterval(() => {
setSeconds((value) => value + 1);
}, 1000);
}
function stop() {
window.clearInterval(intervalRef.current);
intervalRef.current = null;
}
useEffect(() => {
return () => window.clearInterval(intervalRef.current);
}, []);
return (
<div>
<p>{seconds} seconds</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}

Here the interval ID must survive renders but does not belong in the UI, so a ref is appropriate. The displayed elapsed time remains state because changing it must trigger a render.

Refs in React 19

React 19 changed how refs interoperate with components in two important ways:

ref is now a regular prop — forwardRef is no longer required

In function components, ref is now an ordinary prop. You can accept it directly in your props and pass it to a DOM node (or to another component) without wrapping the component in React.forwardRef. forwardRef still works but is no longer necessary, and React plans to deprecate, then remove, it in a future major.

// React 19+: just accept `ref` as a prop.
function FancyInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// Usage is unchanged at the call site:
function Parent() {
const inputRef = useRef(null);
return <FancyInput ref={inputRef} placeholder="Type..." />;
}
Cleanup functions from ref callbacks

Ref callbacks may now return a cleanup function, which React runs when the ref detaches (similar to useEffect). This removes the need for the older "called with null" pattern.

import { useCallback } from 'react';
function reportFocus(event) {
console.log('Focused:', event.currentTarget.name);
}
function FocusableInput() {
const inputRef = useCallback((node) => {
node.addEventListener('focus', reportFocus);
return () => node.removeEventListener('focus', reportFocus);
}, []);
return <input ref={inputRef} name="email" />;
}

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

Which values are appropriate to keep in a ref? Select all that apply.

What is the `useCallback` hook in React and when should it be used?

Topics
React

TL;DR

useCallback caches a function definition while its dependencies remain Object.is-equal. The point is referential stability — so a memo-wrapped child can skip a render, or an Effect that depends on the function does not re-synchronize unnecessarily. Creating a function literal is usually not the cost worth optimizing; use useCallback when the changing identity causes measured or semantically required downstream work.

const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);

What is the useCallback hook in React and when should it be used?

useCallback is an identity cache for a function definition, useful only when stable identity prevents specific downstream work or satisfies a dependency relationship.

What is useCallback?

useCallback caches a function definition between renders until one of its dependencies changes according to Object.is. It does not avoid evaluating the function expression you pass; React returns the previously cached function identity when the dependencies match. That stable identity is useful only when it avoids downstream work or is required by another Hook's dependency relationship.

Syntax

Pass the function and every reactive value it reads:

const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);

When should useCallback be used?

The main cases involve memoized consumers or Effects that depend on the function's identity.

Preventing unnecessary re-renders of memoized children

When you pass a function as a prop to a React.memo-wrapped child, a new function reference on each parent render breaks the memoization and the child re-renders anyway. useCallback keeps the reference stable so React.memo can do its job.

This callback is correct, but its identity changes whenever count changes because it reads that render's state snapshot:

const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);

If the callback only reads count to calculate its next value, the functional updater form removes that dependency and keeps the callback stable across count updates:

import { memo, useCallback, useState } from 'react';
const ParentComponent = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);
return <ChildComponent onClick={handleClick} />;
};
const ChildComponent = memo(({ onClick }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
});
Stabilizing functions used in useEffect deps

If a function is referenced inside an effect's dependency array, a fresh reference each render will cause the effect to re-run every render. Wrapping the function in useCallback (or moving it inside the effect) prevents that:

const handleMessage = useCallback((event) => {
setMessages((messages) => [...messages, event.data]);
}, []);
useEffect(() => {
socket.addEventListener('message', handleMessage);
return () => socket.removeEventListener('message', handleMessage);
}, [socket, handleMessage]);
Relationship to useMemo

useCallback(fn, deps) is exactly equivalent to useMemo(() => fn, deps) — it memoizes a value that happens to be a function. Use useCallback for readability when the value is a function.

Caveats

Memoizing a function adds its own dependency comparison and maintenance cost:

  • It has a cost: useCallback adds a dependency comparison and more code. If no consumer relies on stable identity, leave the function inline.
  • The target is downstream work, not function allocation by itself. Profile the child render, Effect, or other identity-sensitive consumer that memoization is intended to protect.
  • Dependencies must be correct: missing dependencies cause stale closures; over-specified ones defeat the memoization.
  • It is a performance cache: React may discard the cached function for documented reasons such as suspending during the initial mount or editing the component in development. Do not rely on it for correctness.
  • React Compiler may replace it: in a Compiler-enabled codebase, start with plain inline functions and add manual memoization only when the Compiler does not optimize a measured hot path or a specific stable identity is required.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Three parents recreate an onSelect function on an unrelated local update. No other code depends on the callback identity. In which case can stabilizing that function let the expensive child skip rendering?

What is the `useMemo` hook in React and when should it be used?

Topics
React

TL;DR

useMemo caches a calculation result between renders while its dependencies remain Object.is-equal. Use it as a performance optimization for a measured expensive calculation or when a stable object/array identity enables another optimization. It is not a semantic guarantee, so code must remain correct if React discards the cache and recomputes the value. React Compiler can provide equivalent memoization for compatible code when the optional build-time optimizer is enabled.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

What is the useMemo hook in React and when should it be used?

useMemo caches a calculation result as a performance optimization while its reactive dependencies remain equal.

What is useMemo?

The useMemo hook caches the result of a calculation between renders. React returns the cached value while every dependency remains Object.is-equal, unless it discards the cache for a documented reason. Its purpose is performance optimization, not correctness.

Syntax

The syntax for useMemo is as follows:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • The first argument is a function that returns the value you want to memoize.
  • The second argument is an array of dependencies. React normally reuses the value until a dependency changes, but it may discard the cache for documented reasons.

When should it be used?

Use it when profiling identifies meaningful calculation work or when stable value identity enables another measured optimization.

Expensive calculations

For a measured expensive calculation, useMemo can reuse the previous result while its dependencies remain equal. Candidate work includes filtering or sorting a large list, parsing a large payload, or running a heavy synchronous algorithm—not trivial arithmetic.

import { useMemo } from 'react';
const filterAndSortLargeList = (items, query) => {
// Genuinely expensive: scans every item and sorts the result.
return items
.filter((item) => item.name.toLowerCase().includes(query.toLowerCase()))
.toSorted((a, b) => a.name.localeCompare(b.name));
};
const MyComponent = ({ items, query }) => {
const visibleItems = useMemo(
() => filterAndSortLargeList(items, query),
[items, query],
);
return <List items={visibleItems} />;
};
Preserving referential equality for memoized children

This is the most common practical reason to reach for useMemo. Every render produces a new object/array literal, which breaks React.memo (or useEffect dependency) bailouts on a child. Memoizing the value keeps its reference stable across renders.

import { memo, useMemo } from 'react';
const Parent = ({ items }) => {
// Without useMemo, `sortedItems` would be a new array on every render,
// and `MemoChild` would re-render even when `items` is unchanged.
const sortedItems = useMemo(
() => items.toSorted((a, b) => a.name.localeCompare(b.name)),
[items],
);
return <MemoChild sortedItems={sortedItems} />;
};
const MemoChild = memo(function MemoChild({ sortedItems }) {
return (
<ul>
{sortedItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});

Note that useMemo on a value alone does not prevent the child from re-rendering — the child must also be wrapped in memo (or otherwise short-circuit). items.toSorted(...) is used here because Array.prototype.sort mutates in place; mutating a prop would violate React's read-only props contract.

Caveats

Because the cached value is disposable, application correctness cannot depend on the cache being retained:

  • Overuse: Overusing useMemo can lead to more complex code without significant performance benefits. It should be used judiciously.
  • Dependencies: Make sure to correctly specify all dependencies. Missing dependencies can lead to stale values, while extra dependencies can lead to unnecessary recalculations.
  • It is only a cache: React may throw away the value for documented reasons, including editing the component in development or suspending during its initial mount. Never rely on useMemo for correctness.
  • React Compiler changes the calculus: React Compiler 1.0 is a stable, optional build-time optimizer. When enabled, it memoizes compatible components and values, reducing the need for hand-written useMemo and useCallback. Keep explicit memoization when the Compiler is not enabled or when profiling and compiled output show a specific calculation or identity still needs it.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Profiling an unrelated parent update reveals three calculations. Which has the clearest opportunity for useMemo to avoid significant repeated work?

What is the `useReducer` hook in React and when should it be used?

Topics
React

TL;DR

The useReducer hook in React is used for managing complex state logic in functional components. It is an alternative to useState and is particularly useful when the state has multiple sub-values or when the next state depends on the previous one. It takes a reducer function and an initial state as arguments and returns the current state and a dispatch function.

const [state, dispatch] = useReducer(reducer, initialState);

Use useReducer when you have complex state logic that involves multiple sub-values or when the next state depends on the previous state.


What is the useReducer hook in React and when should it be used?

useReducer centralizes state transitions in a pure reducer and exposes a stable dispatch function for sending actions.

Introduction to useReducer

The useReducer hook is a React hook that is used for managing state in functional components. It is an alternative to the useState hook and is particularly useful for managing more complex state logic. The useReducer hook is similar to the reduce function in JavaScript arrays, where you have a reducer function that determines how the state should change in response to actions.

Syntax

The useReducer hook takes two arguments: a reducer function and an initial state. It returns an array with the current state and a dispatch function.

const [state, dispatch] = useReducer(reducer, initialState);

Reducer function

The reducer function is a pure function that takes the current state and an action as arguments and returns the new state. The action is an object that typically has a type property and an optional payload.

function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unhandled action: ' + action.type);
}
}

dispatch feeds actions into the reducer; React stores the returned state and renders again only when the result is not Object.is-equal to the current state:

useReducer update cycle

Example usage

Here is a simple example of using useReducer to manage a counter state:

import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unhandled action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
export default Counter;

Lazy initialization

useReducer accepts an optional third argument: an init function. React uses init(initialArg) to calculate the initial state instead of recalculating it on every render. In development Strict Mode, React calls the initializer twice to expose accidental impurities and ignores one result. This is useful when computing the initial state is expensive or when it must be derived from a prop.

import { useReducer } from 'react';
function init(initialCount) {
return { count: initialCount };
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<button onClick={() => dispatch({ type: 'increment' })}>
Count: {state.count}
</button>
);
}

Bailing out of an update

If the reducer returns a value that is Object.is-equal to the current state, React skips re-rendering the component's children. React may still call the component before ignoring the result, so code must not depend on exactly when the optimization occurs. This is one reason reducers must be pure and must not mutate the existing state object: a mutated-but-same-reference return looks unchanged to React even though memory was modified.

When to use useReducer

Choose a reducer when named transitions make interdependent state updates clearer than separate setter calls:

  • Complex state transitions: Use useReducer when state updates involve multiple sub-values or when the next state depends on the previous one in non-trivial ways. Centralizing the transitions in a reducer is easier to reason about than scattering several useState setters.
  • Explicit, named actions: Dispatching { type: 'increment' } documents intent at the call site; the reducer is the single place that knows how to apply each action.
  • Testability: Because reducers are pure functions of (state, action) -> state, they can be unit-tested in isolation without rendering any components.
  • Stable dispatch identity: React guarantees that the dispatch function has a stable identity across renders. You can safely include it in useEffect/useCallback dependency arrays (or omit it) without causing effects to re-run, and you can pass it down through context without invalidating memoized children.

A common misconception is that useReducer inherently reduces re-renders compared to useState. It does not — both schedule a render whenever the state reference changes. The real wins are around how you structure and reason about updates, plus the stable dispatch identity.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which requirement most strongly favors useReducer over separate useState setters?

What is the `useId` hook in React and when should it be used?

Topics
React

TL;DR

useId (added in React 18) generates a stable, unique string ID per component instance, per React root. Its main reason for existing is to produce IDs that match between the server-rendered HTML and the client hydration — a plain incrementing counter would produce mismatches. Within a single root the IDs are unique, but two separate roots on the same page can collide unless you set identifierPrefix on createRoot / hydrateRoot. Use it for things like linking <label htmlFor> to <input id>, never as a list key.

import { useId } from 'react';
function NameField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Name:</label>
<input id={id} type="text" />
</div>
);
}

What is the useId hook in React and when should it be used?

useId creates component-scoped identifiers for rendered relationships that must remain stable across server rendering, hydration, and later renders.

Introduction to useId

The useId hook was added in React 18. It returns a stable string ID tied to the component instance's position in the tree. The ID is the same on every render of that instance, and — crucially — it is the same on the server and the client.

Why useId exists: SSR and hydration

The actual motivation for useId is server-side rendering. Before React 18, generating IDs with a module-level counter (let next = 0; const id = next++;) could cause hydration mismatches: the server and client might increment the counter in a different order, producing different IDs. React reports mismatches and may regenerate the affected tree on the client, depending on where and how the mismatch occurs.

Module-level counters were already fragile before React 18. Any re-render, multiple renderToString calls sharing module state, or a tree shape that differed between server and client could desynchronize the counter. React 18's concurrent rendering and streaming SSR amplified the problem: Suspense boundaries can resolve out of order, the renderer can pause and resume work, and streamed chunks can arrive at the client in a different order than they were rendered on the server. Any ID source that depends on render order (counters, Math.random() seeded once, mutable module state) inherits this fragility.

useId sidesteps it by deriving the ID from the component's location in the React tree, which is identical on the server and the client regardless of render order. Producing IDs unique to one component instance is a side benefit; the main job is hydration stability.

useId vs other ID-generation approaches

useId exists specifically to be SSR-safe and stable across renders. Common alternatives fail one of those two requirements:

ApproachSSR-safe?Stable across renders?Use when
useId()YesYesComponent-scoped IDs that appear in rendered output.
Math.random()No (server and client diverge, causing hydration mismatch)No (changes every render)Never for render output. Only suitable for non-rendered scratch values.
crypto.randomUUID()No (same reason)NoWhen a globally unique ID needs to be stored in your data, not generated during render.
nanoid / uuid packageNo (when called during render)No (same reason)For IDs persisted in your data model. Generate them outside render and store the result.
Module-level counter incremented during renderNo (server and client may increment in different orders)No (each render increments it again)Do not use it for IDs in rendered output.

Use useId when a component needs to generate an ID for rendered accessibility relationships. An ID supplied through props or stored in application data is also safe to render. If an ID belongs to your data model (a row's primary key, for example), generate it outside render and store it rather than replacing it with useId.

useId in React Server Components

useId behaves inside Client Components the same way it does in plain client React. Two caveats apply specifically to RSC setups:

  • It works in synchronous Server Components but is not supported in async Server Components. If an ID is required inside an async Server Component, generate it outside render or move the rendering into a Client Component.
  • IDs are derived from the component's position in the fiber tree, including any surrounding Suspense boundaries. There is no per-Suspense namespace. The boundary is simply another node in the parent path, which is what keeps IDs in different boundaries distinct.
  • Server Actions are unrelated to ID generation. They run on the server and do not participate in render-time ID generation.

Uniqueness scope and multiple roots

IDs from useId are unique within a single React root. If your page mounts more than one root (for example, an island-style architecture or an embedded widget), separate roots can generate the same opaque ID. Do not depend on the exact generated string: React changed its default prefix in React 19.2.

To prevent that, give each root a distinct identifierPrefix:

import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('widget-a'), {
identifierPrefix: 'a-',
}).render(<WidgetA />);
createRoot(document.getElementById('widget-b'), {
identifierPrefix: 'b-',
}).render(<WidgetB />);

The same option exists on hydrateRoot.

When to use useId

The most common use is associating a <label> with its form control for accessibility:

import { useId } from 'react';
function NameField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Name:</label>
<input id={id} type="text" />
</div>
);
}

It is also useful for ARIA attributes such as aria-describedby, aria-labelledby, and aria-controls.

Pairing inputs with aria-describedby and aria-labelledby

A common useId pattern for accessibility is connecting an input to a hint, an error message, or an external label via aria-describedby or aria-labelledby. Call useId once per component and append suffixes for each related element. This keeps related IDs grouped, saves hook calls, and makes the relationships clear in the markup:

function PasswordField() {
const id = useId();
return (
<>
<label htmlFor={`${id}-password`}>Password:</label>
<input
id={`${id}-password`}
type="password"
aria-describedby={`${id}-hint`}
/>
<p id={`${id}-hint`}>Must be at least 12 characters.</p>
</>
);
}

The same pattern applies to aria-labelledby (when the label is a separate element, not a <label>), aria-controls (a button that toggles a panel), and aria-errormessage (an input pointing at an error region). All of these attributes need stable IDs that survive SSR, which is what useId provides.

What useId is not for

The generated identifier represents a component position, not the identity of application data.

  • Do not use it as a list key. Using useId as a key produces output that renders correctly at first but breaks when the list is reordered or items are inserted. See the callout above for the reason.
  • Avoid it in CSS selectors and document.querySelector. The generated format (e.g. :r0:) contains colons, which are valid in HTML id attributes but require escaping in CSS selectors. Pair <label htmlFor> with <input id> instead of selecting by id.
  • Do not parse or rely on the format. The exact shape (:r0:, «r0», etc.) is an implementation detail and has changed between versions.
  • Do not generate IDs for data with useId. If the ID must outlive the component (saved to a database, sent in an API request, used as a stable record identifier), useId is not the right tool. Use crypto.randomUUID() or a UUID library and store the result.

Practical guidance

Use the opaque value only to connect rendered elements, and store data identities separately:

  • Always pair the generated id with the element via htmlFor / id (or the relevant aria-* attribute) — never use it as decoration.
  • Concatenate suffixes for related controls instead of calling useId repeatedly.
  • Set identifierPrefix on each root if your app mounts more than one.
  • Reach for useId only when you actually need a generated id; many components can simply accept an id prop from the parent.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

Which uses of useId are appropriate? Select all that apply.

What does re-rendering mean in React?

Topics
React

TL;DR

A re-render means React calls a function component again (or calls a class component's render) to compute a new element description. State updates, context changes, or a parent rendering can trigger it. React then reconciles the result with the previous tree. Re-rendering is not the same as updating the DOM: if the rendered output is unchanged, the commit may perform no DOM mutations.


What does re-rendering mean in React?

A re-render is React calling a component again to calculate its next output; it is distinct from committing changes to the host environment.

Understanding re-rendering

Re-rendering is the process by which React calls a component's function again to compute a new UI description. It does not necessarily result in a DOM update: reconciliation may determine that no host changes are required.

When does re-rendering occur?

Re-rendering occurs in the following scenarios:

  • When a state update from a useState setter or useReducer dispatch is not skipped by a same-value bailout
  • When a component subscribed to a context reads a new context value
  • When a parent component re-renders, its children render by default—even if their props are referentially unchanged—unless an applicable bailout skips them
  • When a component's key changes, React unmounts the old instance and mounts a new one (a remount, not just a re-render)

The re-rendering process

React moves from a scheduled update through render and reconciliation before committing any required host changes:

  1. Trigger: A state update, context value change, or parent re-render schedules the component for re-rendering.
  2. Render: React calls the component function again to produce a new React element tree.
  3. Reconciliation: React diffs the new tree against the previous one.
  4. Commit: React applies the required changes (if any) to the actual DOM.

The render and commit phases are separate, so calling a component again does not imply that React will mutate the DOM:

Re-rendering does not always produce a DOM update

Example

Here's a simple example to illustrate re-rendering:

import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

In this example:

  • The Counter component has a state variable count.
  • When the button is clicked, setCount updates the state, triggering a re-render.
  • React calls the Counter function again, producing a new element tree that is reconciled against the previous one.
  • React updates the actual DOM to reflect the new count.

Performance considerations

The cost of a re-render depends on the component work and subtree size. Profile a slow interaction before adding memoization. Common ways to narrow measured render work include:

  • memo: Lets React skip a prop-driven render when each prop compares equal to its previous value with Object.is (unless a custom comparator is provided). The component still renders for its own state or a context value it reads.
  • useMemo and useCallback: Hooks that preserve the identity of values and functions across renders so memoized children don't see "new" props.
  • State colocation: Move state down so fewer components re-render when it changes.

React Compiler

React Compiler 1.0 is an optional build-time optimizer. When enabled, it memoizes compatible components and values and can remove much of the need for manual memo, useMemo, and useCallback. It is not part of React's runtime, does not optimize code it cannot prove safe, and does not eliminate re-renders caused by a component's own state or a context value it reads changing.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

A parent update causes a child function component to run again, but the child returns the same host output as before. What can React do?

What are React Fragments used for?

Topics
React

TL;DR

React Fragments are used to group multiple elements without adding extra nodes to the DOM. This is useful when you want to return multiple elements from a component's render method without wrapping them in an additional HTML element. You can use the shorthand syntax <>...</> or the React.Fragment syntax.

return (
<>
<ChildComponent1 />
<ChildComponent2 />
</>
);

What are React Fragments used for?

Fragments group sibling elements in React's returned tree without adding a host wrapper element to the DOM.

Grouping multiple elements

React Fragments allow you to group multiple elements without adding extra nodes to the DOM. This is particularly useful when you want to return multiple elements from a component's render method but don't want to introduce unnecessary wrapper elements.

Avoiding unnecessary DOM nodes

Using React Fragments helps avoid unnecessary wrapper elements in the DOM.

  • Valid HTML structure: Some elements have strict children rules. A <table> cannot contain a <div> between it and its <tr> rows; a <tr> cannot contain a <div> between it and its <td> cells; a <select> only accepts <option> and <optgroup> children. Fragments let a component return multiple <tr>, <td>, or <option> siblings without breaking the parent's HTML contract.
  • CSS layout integrity: When the parent uses Flexbox or CSS Grid, an extra wrapper <div> becomes a flex/grid item itself and disrupts the layout (the wrapper takes the grid slot, not its children). Fragments keep the children as direct descendants of the layout container.
  • HOC and composition: A higher-order component or render-prop helper that wraps children cannot wrap them in a <div> without breaking layout for every consumer. Returning the children inside a fragment lets the wrapper add behavior without adding DOM.
  • Cleaner markup: Avoiding pointless wrapper <div>s makes the rendered HTML easier to read and style.

Syntax

There are two ways to use React Fragments:

  1. Shorthand syntax: This is the most concise way to use fragments. It uses empty tags <>...</>. The shorthand cannot accept any props, including key — if you need a key (or any other attribute), you must use the explicit React.Fragment form below.

    return (
    <>
    <ChildComponent1 />
    <ChildComponent2 />
    </>
    );
  2. Full syntax: This uses React.Fragment and is required whenever you need to pass a key prop. In stable React 19.2, key is the only accepted prop; Canary builds also support an experimental ref prop.

    import { Fragment } from 'react';
    return (
    <Fragment>
    <ChildComponent1 />
    <ChildComponent2 />
    </Fragment>
    );

When to use <Fragment> instead of <>

The shorthand <>...</> is concise but does not accept props. In stable React, use the explicit React.Fragment form when a key is needed, which happens whenever a map produces items that each render more than one sibling element. Canary builds additionally require the explicit form for the experimental Fragment ref API.

The shorthand version below is incorrect because React cannot attach a key to a <>...</> fragment. React will emit a warning about a missing key:

// Bug: <></> cannot accept a key, so React has no way to identify these fragments
{
items.map((item) => (
<>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</>
));
}

The fix is to use the full Fragment form so you can pass a key:

import { Fragment } from 'react';
{
items.map((item) => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.definition}</dd>
</Fragment>
));
}

In stable React 19.2, key is the only prop Fragment accepts. There is no className, style, or event handler. If any of those are needed, use a real wrapper element instead. Experimental Fragment refs are available only in Canary builds and should be labeled as such.

Use cases

Fragments are useful whenever grouping is required for React structure but an extra host element would be unwanted:

  • Returning multiple elements from a component: When a component needs to return multiple sibling elements, using a fragment can help avoid unnecessary wrapper elements.
  • Rendering lists: When rendering a list of elements, fragments can be used to group the list items without adding extra nodes to the DOM.
  • Conditional rendering: When conditionally rendering multiple elements, fragments can help keep the DOM structure clean.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A list maps each record to two sibling table rows that must share one list key without adding a wrapper element. Which form should it use?

What is `forwardRef()` in React used for?

Topics
React

TL;DR

As of React 19 (December 2024), forwardRef() is no longer necessary — function components can now accept ref as a regular prop, so wrapping in forwardRef() is no longer required. React plans to deprecate forwardRef() in a future release. forwardRef() historically existed because, before React 19, function components could not receive a ref prop and forwardRef() was the official workaround for forwarding a parent's ref down to a child DOM node or component.

// Modern (React 19+): ref is a regular prop
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// Legacy (React 18 and earlier): wrap with forwardRef
import { forwardRef } from 'react';
const MyInputLegacy = forwardRef((props, ref) => (
<input ref={ref} {...props} />
));

What is forwardRef() in React used for?

forwardRef historically let a function component receive a parent's ref and forward it to a child, while React 19 permits ref as a regular prop.

The modern answer (React 19+)

In React 19, ref is just a regular prop on function components. You can destructure it like any other prop and pass it through to a DOM element or child component. There is no longer any reason to reach for forwardRef() in new code:

import { useRef } from 'react';
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
function ParentComponent() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<MyInput ref={inputRef} placeholder="Type here..." />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}

forwardRef() itself still works in React 19.2. The React documentation marks it as no longer necessary and says it will be deprecated in a future release, so migrations can be gradual and should account for the React versions supported by the application or library.

Why forwardRef() existed (legacy context)

Before React 19, ref was a "magic" prop that React intercepted — passing ref to a function component did nothing useful. forwardRef() was the API React provided to opt a function component into receiving a ref alongside its props, so the parent could reach a specific DOM node inside it (e.g. focus an input, measure a node, integrate with imperative third-party libraries).

import { forwardRef, useRef } from 'react';
// Legacy pattern — still works in React 19, but no longer necessary
const MyInput = forwardRef((props, ref) => <input ref={ref} {...props} />);
function ParentComponent() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} placeholder="Type here..." />;
}

useImperativeHandle

When you want to expose a custom imperative API to the parent (rather than the raw DOM node), pair ref with useImperativeHandle. This is unchanged in React 19 — only the ref-forwarding mechanism has changed.

import { useImperativeHandle, useRef } from 'react';
function FancyInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(
ref,
() => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) inputRef.current.value = '';
},
}),
[],
);
return <input ref={inputRef} />;
}

The parent now sees { focus, clear } on the ref instead of the underlying input element. Use this sparingly — it is an escape hatch out of the declarative model.

Things to keep in mind

Ref forwarding exposes an imperative relationship, so its target and version requirements should remain explicit:

  • Class components: A ref attached to a class component receives the class instance directly. They have never needed forwardRef(), and that is unchanged.
  • Forward to a DOM node or imperative handle: A ref must ultimately be attached to a DOM element, a class instance, or an object returned from useImperativeHandle. Forwarding it to another function component just makes that component the new owner of the ref prop.
  • Migration: If you maintain a library that ships forwardRef-wrapped components, dropping the wrapper requires a peer-dependency bump to React 19. Many libraries currently ship both forms during the transition.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A design-system input exposes its DOM input through a ref and promises to support both React 18 and React 19. The team wants one implementation for both. Which migration decision preserves that support?

How do you reset a component's state in React?

Topics
React

TL;DR

The most idiomatic way to reset a component's state in React is to give the component a key prop and change it — React unmounts the old instance and mounts a fresh one with brand-new state. For finer-grained resets, call your useState setter with the initial value, or dispatch a RESET action when using useReducer.

// Force a full reset by changing the key
<Form key={formId} />;
// Or reset specific state in place
setState(initialState);

How do you reset a component's state in React?

Choose between remounting the subtree and updating state in place based on how much component-owned state must be discarded.

Resetting state with a key prop (recommended)

The canonical React-recommended pattern is to pass a different key to the component you want to reset. When the key changes, React treats it as a new component, throws away the old state, refs, and effects, and mounts a fresh instance. This works for any state inside the subtree without the component having to know it's being reset.

import { useState } from 'react';
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
</>
);
}
export default function App() {
const [version, setVersion] = useState(0);
return (
<>
<Form key={version} />
<button onClick={() => setVersion((v) => v + 1)}>Reset form</button>
</>
);
}

This is the same pattern React uses to reset state when switching between items in a list — give each item a stable key and React handles the rest.

Changing the key changes component identity, so React tears down the old subtree before mounting a fresh one:

Resetting state by changing component identity

Resetting useState in place

If you don't need to remount the component, store the initial value and pass it back to the setter. This is fine for small amounts of state but doesn't reset descendant components or refs.

import { useState } from 'react';
const initialState = { count: 0, text: '' };
function MyComponent() {
const [state, setState] = useState(initialState);
const resetState = () => setState(initialState);
return (
<div>
<p>Count: {state.count}</p>
<p>Text: {state.text}</p>
<button onClick={resetState}>Reset</button>
</div>
);
}

Resetting useReducer with a RESET action

When state is managed by a reducer, the conventional approach is to handle a RESET action that returns the initial state. Keep initialState defined outside the component so the reducer and the component share the same source of truth.

import { useReducer } from 'react';
const initialState = { count: 0, text: '' };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'setText':
return { ...state, text: action.value };
case 'reset':
return initialState;
default:
return state;
}
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}

Lazy initializer for derived initial state

If computing the initial state is expensive or depends on props, pass a function to useState (or the third argument to useReducer). React uses the initializer for the initial state, so later re-renders do not recompute it. Development Strict Mode calls a pure initializer twice and ignores one result.

import { useState } from 'react';
function MyComponent({ initialCount }) {
// React uses this function to calculate the initial state. To reset later,
// call the setter with a freshly computed value.
const [state, setState] = useState(() => ({
count: initialCount,
text: '',
}));
const resetState = () => setState({ count: initialCount, text: '' });
return <button onClick={resetState}>Reset</button>;
}

For useReducer, pass (initialArg, init) to do the same:

const [state, dispatch] = useReducer(reducer, initialCount, (count) => ({
count,
text: '',
}));

A note on class components

React recommends function components for new code, but class components remain supported. In an existing class component, reset state with this.setState(...) using a fresh initial value; the key reset pattern also works for an entire class-component subtree.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A multi-step editor has several nested stateful children. Selecting a different document should discard every child’s draft and initialize a fresh editor. What is the most direct React mechanism?

Why does React recommend against mutating state?

Topics
React

TL;DR

React treats state as a read-only snapshot. Mutating an object or array in place keeps the same reference, so an Object.is state bailout, memoized child, memoized calculation, or Effect dependency may not observe a change. Mutation also alters older render snapshots that still reference the object, making behavior harder to reason about and incompatible with features that rely on snapshot semantics. Produce a new object or array with spreads, non-mutating methods such as map/filter/toSorted, or a helper such as Immer.


Why does React recommend against mutating state?

React treats state as an immutable snapshot so queued renders, equality checks, and concurrent work can reason about past and next values independently.

A quick terminology note

In modern React, state is updated by the setter returned from useState (or by dispatch from useReducer). The class-based this.setState API still exists but is rarely used in new code. Both APIs share the same expectation: you give React a new state value rather than mutating the existing one. The rest of this answer focuses on the hooks-based APIs.

Where reference equality actually matters

A common misconception is that mutating state breaks the "virtual DOM diff." That is not quite right — the diff happens against the rendered element tree, not against the state object. The places where state immutability genuinely matters are:

  • Object.is bailout in useState / useReducer: When you call the setter (or return a value from a reducer), React compares the new value with the current one using Object.is. If they are the same reference, React can skip re-rendering the component. Mutate-in-place returns the same reference, so React thinks nothing changed and skips the render — but the data has actually changed, so the UI goes stale.
  • React.memo, useMemo, useCallback, and useEffect dependencies: These all compare values across renders with Object.is. A mutated array or object still has the same reference, so memoized children will not re-render and effects will not re-run, even though their underlying data is now different.
  • Render snapshots and concurrent features: A render conceptually receives a snapshot of props and state. Mutating an object shared by past and in-progress renders changes those snapshots behind React's back, which makes interruptible or restartable rendering unsafe to reason about.
  • History-based tooling: State-management tools such as Redux DevTools can record and replay immutable state snapshots. Mutation overwrites shared objects and makes that history unreliable. React DevTools itself inspects component state but does not provide Redux-style time travel.

How the state bailout behaves

When the next state is Object.is-equal to the current state, React skips re-rendering the component and its children. React may still call the component before applying that optimization in some cases, so code must not depend on exactly when the bailout occurs. Mutation is a problem because it produces "same value to React, changed data in memory."

Mutation and immutable replacement take opposite paths through the reference-equality check:

Mutation can hide a state change from React

Problems with mutating state

Mutation can hide updates from React and change values that earlier renders still conceptually own:

  1. Stale UI updates: The bailout above means the UI does not refresh when the underlying data changed.
  2. Broken memoization: Memoized children, memoized values, and effects all compare by reference and will silently skip updates.
  3. Broken snapshot semantics under interruptible or restartable rendering.
  4. Lost debuggability: Previous snapshots and history-based state tooling no longer represent what the application rendered.
  5. Hard-to-track bugs: Multiple components or hooks may close over the same object reference. Mutating it can have spooky action-at-a-distance effects.

How to update state correctly

Always produce a new value:

const [user, setUser] = useState({ name: 'Ada', age: 36 });
// Incorrect: mutates the existing object — same reference, bailout fires.
user.age = 37;
setUser(user);
// Correct: a brand new object.
setUser({ ...user, age: 37 });
// Equally correct, and safer when the new value depends on the old one
// (avoids stale-closure issues across batched updates):
setUser((prev) => ({ ...prev, age: 37 }));

For arrays, prefer non-mutating methods like map, filter, concat, the spread operator, or the newer toSorted/toReversed/toSpliced (avoid push, splice, sort, reverse on state):

const [items, setItems] = useState([3, 1, 2]);
// Incorrect: sort() mutates in place.
items.sort();
setItems(items);
// Correct.
setItems([...items].sort((a, b) => a - b));
// Or, with the modern non-mutating method:
setItems(items.toSorted((a, b) => a - b));

When the spread gets painful: Immer

Spreading deeply nested state by hand is verbose and error-prone. Immer is the de facto solution: you write code that looks like mutation against a draft, and Immer produces a new immutable state for you. It is built into Redux Toolkit's createSlice, and the useImmer / useImmerReducer hooks plug directly into React.

import { produce } from 'immer';
setUser((prev) =>
produce(prev, (draft) => {
draft.address.city = 'Singapore';
}),
);

You get the ergonomics of mutation and the guarantees of immutability.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A component runs this handler. What is the central problem?

function addItem(item) {
cart.items.push(item);
setCart(cart);
}

What are error boundaries in React for?

Topics
React

TL;DR

Error boundaries catch errors thrown while rendering their descendant tree and display fallback UI instead of losing the entire React root. They are class components that use static getDerivedStateFromError to render a fallback and may use componentDidCatch for logging. Place them at meaningful recovery boundaries such as routes or independent panels. They do not catch event-handler errors, errors in arbitrary asynchronous callbacks, server-rendering errors, or errors thrown by the boundary itself. React has no function-component API for defining a boundary; a library can provide a reusable boundary component. React 19 also provides root options for reporting caught, uncaught, and recoverable errors, but those reporting callbacks do not replace fallback UI.


What are error boundaries in React for?

Error boundaries contain render-time failures in a subtree and replace that subtree with fallback UI instead of losing the entire interface.

Introduction

Error boundaries catch render-time errors in their descendant component tree. They can report the error and replace only the failed subtree with fallback UI instead of allowing the error to unmount the entire root.

Specifically, an error boundary catches errors thrown during:

  • Rendering of its descendants
  • Lifecycle methods of its descendants
  • Constructors of its descendants

Since React 16, if an error is not caught by any boundary, React unmounts the entire component tree from the root. In production, place boundaries where the application can present a useful fallback and let unaffected areas remain interactive.

How to implement error boundaries

As of React 19, error boundaries must still be class components — there is no hooks-based equivalent. To create one, define a class component that implements at least one of the following methods:

  • static getDerivedStateFromError(error): Updates state so the next render shows the fallback UI. This alone is sufficient to render a fallback.
  • componentDidCatch(error, info): Used to log error information to an error reporting service. It is optional and not required to render a fallback.

Here is an example of an error boundary component:

import React, { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render shows the fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
console.error('Error caught by ErrorBoundary: ', error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;

Usage

To use the error boundary, wrap it around any component that you want to monitor for errors:

<ErrorBoundary>
<MyComponent />
</ErrorBoundary>

The boundary contains only its descendant subtree; siblings outside that boundary can remain mounted and interactive:

Error containment at the nearest boundary

Limitations

Error boundaries have some limitations:

  • They do not catch errors inside event handlers. For event handlers, you need to use regular JavaScript try/catch blocks.
  • They do not catch errors in asynchronous code (e.g., setTimeout or requestAnimationFrame callbacks).
  • They do not catch errors during server-side rendering.
  • They do not catch errors thrown in the error boundary itself — those propagate up to the next error boundary above it in the tree (or unmount the whole root if none exists).

Root-level error handlers (React 19)

React 19 added reporting options on createRoot and hydrateRoot, useful for centralized logging and analytics. onCaughtError reports errors handled by a boundary, while onUncaughtError reports errors that reached the root without one:

import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'), {
onUncaughtError: (error, errorInfo) => {
// Errors not caught by any error boundary
console.error('Uncaught error:', error, errorInfo.componentStack);
},
onCaughtError: (error, errorInfo) => {
// Errors caught by an error boundary
console.error('Caught error:', error, errorInfo.componentStack);
},
onRecoverableError: (error, errorInfo) => {
// Errors React recovered from automatically (e.g. hydration mismatches)
console.error('Recoverable error:', error, errorInfo.componentStack);
},
});

These complement error boundaries — they do not replace them.

The react-error-boundary library

If you do not want to maintain a class wrapper, the react-error-boundary library is one reusable option. It exposes an <ErrorBoundary> component plus a useErrorBoundary hook for forwarding errors from function components to the nearest boundary.

Best practices

Place boundaries according to the failures users should be able to recover from independently:

  • Place error boundaries around route content or independent sections that can fail and recover separately.
  • Log errors to an error reporting service to keep track of issues in production.
  • Give the fallback an actionable recovery path, such as retrying, navigating away, or reloading the affected data.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which failures can a React error boundary catch and replace with fallback UI? Select all that apply.

How do you test React applications?

Topics
React

TL;DR

A common stack is Jest or Vitest as the test runner with React Testing Library, which encourages testing observable behavior instead of implementation details. Drive realistic interactions with @testing-library/user-event, mock network calls with a tool such as MSW, and cover critical flows in a real browser with Playwright or Cypress. Use async queries (findBy*, waitFor) for UI that appears after asynchronous client updates. Test Server Components through the integration facilities of the framework and bundler that implement them.


How do you test React applications?

A balanced React test suite checks user-visible behavior at component boundaries and reserves browser tests for critical integrated flows.

Unit testing

Unit testing covers individual components in isolation. Jest and Vitest are the two dominant runners — Vitest is the natural choice for Vite-based projects and has largely caught up with Jest in features. Both pair with React Testing Library, which renders components and queries the DOM the way a user would.

Add @testing-library/jest-dom once in your setup file (e.g. setupTests.ts) so its matchers are registered globally — the older @testing-library/jest-dom/extend-expect import path is no longer needed:

// setupTests.ts
import '@testing-library/jest-dom';
// MyComponent.test.tsx
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';
test('renders the component with the correct text', () => {
render(<MyComponent />);
expect(screen.getByText('Hello, World!')).toBeInTheDocument();
});

Integration testing

Integration tests exercise multiple components together. Prefer @testing-library/user-event over fireEvent — it simulates real user interactions (focus, hover, typing) much more faithfully and returns a promise, so you await the action.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ParentComponent from './ParentComponent';
test('updates child component when parent state changes', async () => {
const user = userEvent.setup();
render(<ParentComponent />);
await user.click(screen.getByRole('button', { name: 'Update Child' }));
expect(await screen.findByText('Child Updated')).toBeInTheDocument();
});

Note findByText (async) instead of getByText for assertions on UI that appears after an update — findBy* retries until the element appears or times out, and is the standard tool for anything asynchronous. For more complex polling, use waitFor.

Testing custom hooks

Use renderHook from @testing-library/react to test hooks in isolation:

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
test('increments the counter', () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});

Mocking network requests with MSW

Mock Service Worker (MSW) intercepts requests at the network layer rather than stubbing fetch, so the same handlers can be reused in unit tests, Storybook, and the dev server. It is a widely used option for API mocking in React tests.

import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/user', () => HttpResponse.json({ name: 'Ada' })),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Testing asynchronous UI and Server Components

For client UI that updates after a request, timer, or transition, trigger the behavior and await the visible result with findBy* or waitFor. Do not make the Client Component itself an async function; client-side suspension should use a supported Suspense-enabled data source.

React Server Components rely on a framework or bundler to produce and consume their serialized payload. Calling a Server Component as an ordinary async function does not test that pipeline. Prefer the framework's integration or end-to-end testing setup, and unit-test pure data helpers separately. Keep network calls controlled at an appropriate boundary, for example with MSW or a stubbed data module.

test('renders user data once loaded', async () => {
render(<UserProfile id="42" />);
expect(await screen.findByText('Ada')).toBeInTheDocument();
});

End-to-end testing

End-to-end tests drive the whole application in a real browser. Playwright is a common choice for new projects and includes parallel execution, multi-browser support, auto-waiting, and a trace viewer. Cypress is also widely used and is a fine option, especially in an existing codebase.

// Playwright
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Username').fill('user');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/dashboard/);
});

Snapshot testing

Snapshot testing captures the rendered output of a component and compares it to a saved snapshot on subsequent runs. With React 19, react-test-renderer is deprecated — render with React Testing Library instead and snapshot the resulting markup:

import { render } from '@testing-library/react';
import MyComponent from './MyComponent';
test('matches the snapshot', () => {
const { asFragment } = render(<MyComponent />);
expect(asFragment()).toMatchSnapshot();
});

Use snapshot tests sparingly — they're easy to update reflexively, which can let regressions slip through. Reserve them for stable presentational output.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which testing practices focus on observable React behavior? Select all that apply.

Explain what React hydration is

Topics
React

TL;DR

Hydration is the process in which React renders the client tree, matches it to HTML previously produced by React on the server, attaches event handling, and makes the existing markup interactive. Use hydrateRoot rather than creating a fresh client root over server HTML. The first client output must match the server output; mismatches are bugs unless they are deliberately and narrowly suppressed.


What is React hydration?

Hydration is the client phase that connects a React tree to matching HTML previously rendered on the server.

Server-side rendering (SSR)

Server-side rendering (SSR) generates a page's initial HTML on the server and sends it to the browser. It makes content available before application JavaScript runs and exposes that content directly to crawlers, although its performance and search impact depend on the rest of the delivery architecture.

Hydration process

Hydration is the process that happens after the server-side rendered HTML is sent to the client. React takes the static HTML and "hydrates" it by attaching event listeners and initializing the state, making the page interactive. This process involves:

  1. Matching and reusing existing HTML: React renders the component tree on the client but reuses matching server-created DOM nodes instead of replacing them all.
  2. Attaching event listeners: React attaches the necessary event listeners to the existing HTML elements.
  3. Initializing state: React initializes the component state and props to make the page dynamic.

Hydration adopts the server-created DOM only when the first client render describes matching output:

Server rendering and client hydration

Example

Here's a simple example to illustrate the concept:

  1. Server-side rendering: The server generates the following HTML:

    <div id="root">
    <button>Click me</button>
    </div>
  2. Client-side hydration: When the HTML is sent to the client, React hydrates it with the following code:

    import { hydrateRoot } from 'react-dom/client';
    function App() {
    const handleClick = () => {
    alert('Button clicked!');
    };
    return <button onClick={handleClick}>Click me</button>;
    }
    hydrateRoot(document.getElementById('root'), <App />);

In this example, the server sends the static HTML with a button to the client. React then hydrates the button and makes its onClick behavior interactive. ReactDOM.hydrate was deprecated in React 18 and removed in React 19; use hydrateRoot from react-dom/client.

Selective and streaming hydration (React 18+)

React 18 introduced selective hydration powered by Suspense. With streaming SSR (renderToPipeableStream / renderToReadableStream), the server can flush HTML in chunks as data becomes ready, and the client can hydrate parts of the tree independently — a slow Suspense boundary no longer blocks the rest of the page from becoming interactive. React also prioritizes hydrating the part of the tree the user is currently interacting with.

import { Suspense } from 'react';
function Page() {
return (
<>
<Header />
<Suspense fallback={<Skeleton />}>
<Comments />
</Suspense>
<Footer />
</>
);
}

Stable IDs across server and client

Generating IDs (e.g. for aria-labelledby or form htmlFor) with Math.random() or counters causes hydration mismatches because the server and client produce different values. Use the useId hook to get a stable, deterministic ID that matches on both sides.

import { useId } from 'react';
function Field() {
const id = useId();
return (
<>
<label htmlFor={id}>Name</label>
<input id={id} />
</>
);
}

Suppressing intentional mismatches

If an element's text or attribute is unavoidably different between server and client, suppressHydrationWarning can silence that one-level mismatch. Use it sparingly: it is an escape hatch, does not work recursively, and React does not attempt to patch mismatched text content covered by it.

<time suppressHydrationWarning>{new Date().toISOString()}</time>

What hydration enables

Hydration preserves matching server-created DOM while React connects it to the client component tree. This makes server-rendered controls interactive without discarding and rebuilding the whole document. The earlier visible content and crawlable response are benefits of prerendering; hydration is the client work needed to activate that HTML.

Challenges of hydration

Hydration requires deterministic initial output and adds client work proportional to the interactive tree.

  1. Mismatch issues: If the server-rendered HTML does not match the client-side React components, React logs an error. React 19 improved hydration diagnostics by consolidating mismatch information and showing a diff of relevant attributes or text, which gives a more precise starting point for debugging.
  2. Visible before interactive: Server-rendered content can appear before the JavaScript loads and hydration connects React event handlers. During this gap, a button may look ready but its React click handler cannot run yet. Earlier visible content does not guarantee earlier interactivity; the delay depends on JavaScript loading and hydration work.
  3. Performance overhead: Hydration can be resource-intensive on large pages. Selective hydration and breaking the tree into Suspense boundaries help spread the cost.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A server prints a timestamp in a React page. The browser must show that same timestamp initially and may update it afterward. Which implementation meets both requirements?

What are React Portals used for?

Topics
React

TL;DR

React Portals are used to render children into a DOM node that exists outside the hierarchy of the parent component. This is useful for scenarios like modals, tooltips, and dropdowns where you need to break out of the parent component's overflow or z-index constraints. You create a portal with createPortal(child, container) from react-dom. Even though the rendered DOM lives elsewhere, the portal still belongs to the React tree, so events bubble up to the React parent and context still flows through normally.


What are React Portals used for?

Portals separate a child's DOM placement from its ownership in the React component tree.

Introduction

React Portals provide a way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. This is particularly useful for certain UI patterns that require breaking out of the normal parent-child DOM structure.

Use cases

Overlays are the most common portal use case because they often need to escape clipping and stacking contexts:

Modals

Modals often need to be rendered outside the parent component to avoid issues with z-index and overflow. By using a portal, you can ensure the modal is rendered at the top level of the DOM, making it easier to manage its visibility and positioning.

import { createPortal } from 'react-dom';
function Modal({ isOpen, children, titleId }) {
if (!isOpen) return null;
return createPortal(
<div
aria-labelledby={titleId}
aria-modal="true"
className="modal"
role="dialog">
{children}
</div>,
document.getElementById('modal-root'),
);
}

This abbreviated example only demonstrates portal placement. A production modal must also manage focus, dismissal, and background interaction as described in the accessibility section below.

Tooltips

Tooltips need to be rendered outside the parent component to avoid being clipped by overflow settings. Using a portal lets the tooltip escape ancestor overflow: hidden and stacking-context constraints. To position the tooltip relative to its target element, use getBoundingClientRect() (which gives viewport-relative coordinates) and add the current scroll offsets — offsetTop / offsetLeft are relative to the nearest positioned ancestor (offsetParent) and produce the wrong values once the tooltip is portaled into document.body.

import { useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
const Tooltip = ({ text, targetRef }) => {
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
const target = targetRef.current;
if (!target) return;
function updatePosition() {
const rect = target.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
});
}
updatePosition();
window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition, true);
const resizeObserver = new ResizeObserver(updatePosition);
resizeObserver.observe(target);
return () => {
window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true);
resizeObserver.disconnect();
};
}, [targetRef]);
return createPortal(
<div className="tooltip" style={{ position: 'absolute', ...position }}>
{text}
</div>,
document.body,
);
};
Dropdowns and popovers

Dropdown menus, comboboxes, and other popover-style UIs benefit from portals for the same reason as tooltips: they often need to escape an ancestor's overflow: hidden, transform, or stacking context. A portal lets the menu render at the top of the DOM while still being part of the React tree, which means clicks and keyboard events still bubble up to the parent dropdown component for state handling.

import { createPortal } from 'react-dom';
const Dropdown = ({ isOpen, children }) => {
if (!isOpen) return null;
return createPortal(
<div className="dropdown">{children}</div>,
document.body,
);
};

How to create a portal

To create a portal, import createPortal from react-dom. It takes the React content to render and the DOM node to render it into.

import { createPortal } from 'react-dom';
createPortal(child, container);

Event bubbling follows the React tree

A subtle but commonly tested point: even though the portal's DOM node lives elsewhere in the document, React events (onClick, onChange, etc.) bubble up through the React component tree, not the DOM tree. So a click inside a portaled modal will trigger an onClick handler on the modal's React parent, even if that parent is on the other side of the document. Context also flows through portals normally.

The child therefore occupies one position for React ownership and another for physical DOM placement:

A portal preserves the React tree while changing the DOM tree

Server-side rendering

createPortal requires a real DOM node, which doesn't exist during server-side rendering. A common pattern is to initialize a mounted state to false, set it to true in an Effect, and return null until then. The server and the client's first render consequently match. A typeof window !== 'undefined' branch directly in render can instead produce different initial markup and cause a hydration mismatch.

Accessibility

Portals are commonly used for modals and dialogs, which come with accessibility expectations: focus should move into the dialog when it opens and be trapped inside it (Tab/Shift+Tab cycles within the dialog), Escape should close it, and focus should return to the triggering element on close. The dialog itself needs an appropriate role (role="dialog" or the native <dialog> element) and an accessible label. Implementing focus traps correctly is non-trivial, so most teams rely on dialog primitives from libraries like Base UI, Ariakit, or React Aria rather than rolling their own.

Benefits

Portals solve placement constraints while preserving React relationships such as context and event propagation.

  • Breaking out of parent constraints: Portals allow you to render components outside the parent component's DOM hierarchy, which is useful for avoiding issues with z-index, overflow, and transform-based stacking contexts.
  • Preserves the React tree: Events bubble and context flows through portals as if the children were still nested under the parent component, so the rest of your code keeps working the way you'd expect.
  • Independent overlay layer: Rendering overlays into a shared top-level container makes their stacking and positioning rules easier to manage, although the overlay still needs explicit styles and interaction behavior.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 4
Check your understanding Exercise 1 of 4

A menu is logically owned by a toolbar button but is clipped by the toolbar's overflow: hidden. The toolbar has position: relative and establishes the menu's containing block. Which design preserves the React relationship while escaping that DOM clipping boundary?

How do you debug React applications?

Topics
React

TL;DR

Use React Developer Tools to inspect the component tree and profile commits, browser breakpoints and network tools for runtime behavior, and development Strict Mode to expose impure rendering and missing cleanup. Owner stacks help trace which component created failing JSX. React 19.2 also adds React tracks to Chrome performance profiles for scheduler and component work. Use error boundaries for render failures, while handling event and asynchronous errors at their source.


How do you debug React applications?

Effective React debugging combines component-aware tools with ordinary browser runtime, network, and performance inspection.

Using React Developer Tools

The React Developer Tools browser extension is the primary tool for inspecting and debugging React apps. The two tabs:

  • Components — inspect the component tree, view/edit props and state and hooks live, jump to the component's source, and use the "Log to console" / "View source" buttons. Toggle "Highlight updates when components render" to spot wasted re-renders visually.
  • Profiler — record an interaction and see a flamegraph of every commit, which components rendered, how long each took, and why each rendered (props changed, state changed, hooks changed, parent re-rendered). Indispensable for diagnosing performance issues.

Follow the official React Developer Tools guide for current browser-extension and standalone installation links.

Owner stacks (React 19.1)

React 19.1 introduced owner stacks, which describe the components that created a failing element rather than every component through which it later rendered. In development, captureOwnerStack() can capture this information during render, Effects, React event handlers, and React error handlers. It returns null outside supported React-controlled execution and is unavailable in production builds.

React Performance Tracks (React 19.2)

React 19.2 adds Scheduler and Components tracks to Chrome DevTools performance profiles. They show update priorities, renders, effects, blocked work, and the relationship between React work and browser tasks. Use them when the React Profiler identifies a slow interaction but you need to correlate it with the rest of the page's performance timeline.

Strict Mode

Wrap your tree in <StrictMode> during development to surface common bugs early. React re-renders components an extra time, re-runs Effects through an extra setup/cleanup cycle, and re-runs ref callbacks so you notice impure renders and missing cleanup before they ship. These checks do not run in production.

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);

Logging and breakpoints

Plain console.log still works, but a few React-specific habits make it more useful:

  • Right-click a component in the DevTools Components tab and pick Log component data to console to dump its props, state, and hooks without editing source.
  • Use debugger statements inside a render or effect — DevTools pauses there and you can inspect closures and hook call order.
  • In Chrome DevTools, enable "Pause on uncaught exceptions" and "Pause on caught exceptions" when chasing an error you can't reproduce reliably.
  • For tracking why a component re-renders, prefer the DevTools Profiler over hand-rolled logs. The third-party why-did-you-render library can add diagnostics when the built-in tools are insufficient. React Compiler may reduce prop-driven re-renders when enabled, but it does not make profiling unnecessary.

Using error boundaries

Error boundaries are React components that catch JavaScript errors in their child component tree. You can implement error boundaries in two ways:

Using React's built-in class component

React's built-in error-boundary API is implemented with class lifecycle methods:

import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
// You can also log the error to an error reporting service
console.error('Error caught by error boundary:', error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}
Using the react-error-boundary package

Alternatively, you can use the react-error-boundary package for a more convenient approach:

import { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { reportError } from './error-reporting';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{ color: 'red' }}>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
const [retryKey, setRetryKey] = useState(0);
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => setRetryKey((key) => key + 1)}
onError={reportError}>
<MyComponent key={retryKey} />
</ErrorBoundary>
);
}

For handling errors in event handlers or async code, you can use the useErrorBoundary hook:

import { useErrorBoundary } from 'react-error-boundary';
import { saveChanges } from './api';
function MyComponent() {
const { showBoundary } = useErrorBoundary();
const handleAsyncError = async () => {
try {
await saveChanges();
} catch (error) {
showBoundary(error);
}
};
return <button onClick={handleAsyncError}>Save changes</button>;
}

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A search panel becomes sluggish after several interactions, and the team suspects React is rendering too much. How would you verify that diagnosis and find the trigger?

What is React Strict Mode and what are its benefits?

Topics
React

TL;DR

<StrictMode> enables development-only checks for a React subtree. React renders components an extra time to expose impure rendering, runs an extra setup-and-cleanup cycle for Effects, re-runs ref callbacks, and checks for deprecated APIs. It renders no UI and adds no production behavior. The extra work is a stress test: fix the missing cleanup or impurity instead of trying to suppress it.


What is React Strict Mode and what are its benefits?

Strict Mode enables development-only checks that expose rendering impurities, missing Effect cleanup, and deprecated API usage.

What is React Strict Mode?

React strict mode is a feature in React that helps developers identify potential problems in their applications. It is a wrapper component that you can use to wrap parts of your application to enable additional checks and warnings. It does not render any visible UI and does not affect the production build.

To use React strict mode, you can wrap your component tree with the StrictMode component:

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>,
);

Some frameworks enable Strict Mode for you. Check the framework and router version rather than assuming it is active; otherwise add <StrictMode> at the root or around the subtree you want to check.

Benefits of React Strict Mode

Its extra development executions are diagnostics that make assumptions about purity and cleanup fail earlier.

Detecting unexpected side effects (the headline behavior)

In development, Strict Mode performs extra checks including:

  • Rendering function components (and selected class methods) an extra time
  • Calling functions that should be pure, such as state initializer/updater functions, reducers, and useMemo calculations, an extra time
  • Running one extra Effect setup-and-cleanup cycle
  • Running one extra ref-callback setup-and-cleanup cycle

The Effect check is a common interview topic. It surfaces Effects that are not safely re-synchronized—for example, an Effect that subscribes but does not unsubscribe in cleanup. React is stress-testing the Effect's setup/cleanup contract; it is not performing a second production mount.

These checks apply only in development. If <StrictMode> is not enabled at the root, React does not run the initial extra Effect cycle for a nested boundary because that parent/child sequence could not happen in production.

At a Strict Mode root, the initial development sequence deliberately repeats pure work and immediately verifies that Effect cleanup reverses setup:

Initial Strict Mode development checks
Warning about legacy and deprecated APIs

StrictMode warns about APIs that are no longer recommended, including:

  • The legacy string ref API (use useRef instead — that's the modern default in function components)
  • Use of findDOMNode
  • Deprecated context APIs
Surfacing unsafe class lifecycle methods

In legacy class components, StrictMode flags componentWillMount, componentWillReceiveProps, and componentWillUpdate as unsafe. This matters less today since most new code uses function components, but it's still relevant if you're maintaining a class-based codebase.

Ensuring components are resilient to future changes

The additional checks make it easier to spot components that quietly depend on a single mount or hidden side effects, so your codebase stays compatible as React adds features that may mount, unmount, and remount components more aggressively.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A development Effect connects to a service, disconnects, and connects again immediately after mount under <StrictMode>. What should the team do?

How do you localize React applications?

Topics
ReactInternationalization

TL;DR

To localize a React application, you typically use a library like react-i18next or react-intl. First, you set up your translation files for different languages. Then, you configure the localization library in your React app. Finally, you use the provided hooks or components to display localized text in your components.

// Example using react-i18next
import { useTranslation } from 'react-i18next';
const MyComponent = () => {
const { t } = useTranslation();
return <p>{t('welcome_message')}</p>;
};

Setting up localization in React

Localization combines message lookup with locale selection, plural rules, formatting, layout direction, and server/client consistency.

Choosing a localization library

There are several libraries available for localizing React applications, including react-i18next and react-intl. The examples below use react-i18next.

Installing the library

First, install the necessary packages:

npm install i18next react-i18next

Setting up translation files

Create JSON files for each language you want to support. For example, create en.json and fr.json in a locales directory:

locales/en.json:

{
"welcome_message": "Welcome to our application!"
}

locales/fr.json:

{
"welcome_message": "Bienvenue dans notre application!"
}

Configuring the localization library

Set up i18next and react-i18next in your application. Create an i18n.js file for the configuration:

// i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './locales/en.json';
import fr from './locales/fr.json';
i18n.use(initReactI18next).init({
resources: {
en: { translation: en },
fr: { translation: fr },
},
lng: 'en', // default language
fallbackLng: 'en',
interpolation: {
escapeValue: false, // React is safe from XSS by default — it escapes string children. Only `dangerouslySetInnerHTML` bypasses that, so don't pass translated HTML through it without sanitization.
},
});
export default i18n;

Integrating with your React application

Because initReactI18next already registers the i18n instance with React, you no longer need to wrap your tree in I18nextProvider (only use it when you want to scope a different instance to a subtree). Just import the config once at your entry and render with createRoot:

// index.js
import { createRoot } from 'react-dom/client';
import App from './App';
import './i18n'; // side-effect import: initializes i18next
const root = createRoot(document.getElementById('root'));
root.render(<App />);

Using translations in components

Use the useTranslation hook to access the t function for translating text:

// MyComponent.js
import { useTranslation } from 'react-i18next';
const MyComponent = () => {
const { t } = useTranslation();
return <p>{t('welcome_message')}</p>;
};
export default MyComponent;

Switching languages

To switch languages, use the i18n.changeLanguage method:

// LanguageSwitcher.js
import { useTranslation } from 'react-i18next';
const LanguageSwitcher = () => {
const { i18n } = useTranslation();
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
};
return (
<div>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('fr')}>Français</button>
</div>
);
};
export default LanguageSwitcher;

Interpolation and variables

Most translations need to inject runtime values. Pass an options object as the second argument to t:

locales/en.json:

{
"greeting": "Hello, {{name}}!"
}
const { t } = useTranslation();
return <p>{t('greeting', { name: user.name })}</p>;

For rich text with embedded React elements (e.g. links inside a sentence), use the Trans component so translators can reorder children without breaking JSX:

import { Trans } from 'react-i18next';
<Trans i18nKey="terms">
By continuing you accept our <a href="/terms">terms</a>.
</Trans>;

Pluralization

i18next picks the correct plural form based on the active language's CLDR rules. Define keys with the _one, _other, _zero, _few, _many suffixes and pass count:

{
"items_zero": "No items",
"items_one": "{{count}} item",
"items_other": "{{count}} items"
}
t('items', { count: cart.length });

Namespaces

Splitting translations into namespaces (one JSON file per feature) keeps bundles small and avoids key collisions. Load only the namespaces a component needs:

const { t } = useTranslation(['checkout', 'common']);
t('checkout:placeOrder');
t('common:cancel');

Lazy-loading translations

Shipping every locale to every user wastes bandwidth. Use i18next-http-backend to fetch translation files on demand and i18next-browser-languagedetector to pick the user's language automatically:

import i18n from 'i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
react: { useSuspense: true },
});

With Suspense mode enabled in the same initialization call, components can render a fallback while a translation file loads:

import { Suspense } from 'react';
<Suspense fallback={<Spinner />}>
<App />
</Suspense>;

Right-to-left (RTL) layout

For languages like Arabic or Hebrew, set the document direction when the language changes and let CSS logical properties (margin-inline-start, padding-inline-end, etc.) handle the rest:

useEffect(() => {
document.documentElement.dir = i18n.dir(); // 'ltr' or 'rtl'
document.documentElement.lang = i18n.language;
}, [i18n.language]);

Date, number, and currency formatting

Don't hand-format numbers or dates. Use the built-in Intl APIs, which respect the locale's conventions for separators, ordering, and currency symbols:

new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(1234.5); // "1.234,50 €"
const date = new Date('2026-04-23T00:00:00Z');
new Intl.DateTimeFormat('ja-JP', {
dateStyle: 'long',
timeZone: 'UTC',
}).format(date);
// "2026年4月23日"
new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-1, 'day');
// "yesterday"

i18next can call these via its built-in formatter: t('updated', { date, formatParams: { date: { dateStyle: 'long' } } }).

SSR and Next.js considerations

Server rendering adds request isolation and serialization constraints to the localization setup:

  • For Next.js App Router, use next-intl or i18next with the i18next/react-i18next SSR setup. Initialize i18next per request so the server doesn't leak one user's language to another.
  • Send the active language in the initial HTML (<html lang="...">) and serialize the loaded resources so the client can hydrate without a flash of untranslated content.
  • Server Components can translate after the request-specific i18n instance is initialized. Pass translated strings or other serializable data to Client Components; a t function cannot cross the Server-to-Client Component boundary because functions are not serializable.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

A checkout has lang="ar", but its form still flows left to right. Its English CSS uses margin-left to separate labels from icons. What should change at the document and layout boundaries?

What is code splitting in a React application?

Topics
React

TL;DR

Code splitting in a React application is a technique used to improve performance by splitting the code into smaller chunks that can be loaded on demand. This helps in reducing the initial load time of the application. You can achieve code splitting using dynamic import() statements or React's React.lazy and Suspense.

import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}

What is code splitting in a React application?

Code splitting creates separately loadable JavaScript chunks so a route or feature does not have to ship in the initial bundle.

Introduction

Code splitting moves route- or feature-specific modules into separately loadable chunks. It can reduce the JavaScript downloaded, parsed, and executed for the initial route when deferred chunks are not immediately needed. Each split also adds a request and a possible loading state, so choose boundaries based on bundle analysis and real navigation behavior.

The build creates the boundary, and the runtime fetches the deferred chunk only when navigation or rendering reaches it:

Build-time splitting and runtime lazy loading

How to implement code splitting

Split points can be introduced directly with dynamic imports or through React and framework conventions built on them.

Using dynamic import()

Dynamic import() is a JavaScript feature that allows you to load modules asynchronously. This can be used to split your code into separate chunks.

async function formatReport(data) {
const { formatReportRows } = await import('./report-formatters');
return formatReportRows(data);
}
Using lazy and Suspense

React provides built-in support for code splitting through lazy and Suspense. lazy lets you render a dynamic import as a regular component, and Suspense lets you specify a loading fallback while the chunk is being fetched.

import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
Route-based splitting

The dominant real-world use case is splitting at route boundaries — each route loads its own chunk so users only download the code for the page they visit. With React Router this is typically:

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Framework-level splitting

React frameworks usually provide route-level code splitting and preloading conventions. For example, the Next.js App Router automatically splits application code by route segments, while React Router framework mode creates route chunks according to its build configuration. Follow the framework's current guidance before adding lazy around routes yourself; lazy remains useful for optional client-only features such as editors, charts, and rarely opened dialogs.

Pairing Suspense with error boundaries

Suspense only handles the loading state. If the dynamic import fails because of a network error or deploy mismatch, an error boundary can render a failure fallback. This example uses the third-party react-error-boundary package:

import { ErrorBoundary } from 'react-error-boundary';
function Feature() {
return (
<ErrorBoundary fallback={<p>Could not load this feature.</p>}>
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}
Suspense for data and use()

In React 19, a component can read a cached promise with the special use API and suspend at the same boundary used by lazy. The promise should come from a Suspense-enabled framework or cache rather than being recreated during render. A single <Suspense> boundary can then coordinate supported code and data dependencies.

Preloading

With a bundler that caches the same dynamic import, an application can start loading a lazy chunk before it is needed, such as on hover or keyboard focus:

const Editor = lazy(() => import('./Editor'));
function preloadEditor() {
void import('./Editor');
}
function OpenButton() {
return (
<button
onFocus={preloadEditor}
onMouseEnter={preloadEditor}
onClick={openEditor}>
Open editor
</button>
);
}

Benefits of code splitting

Useful split points reduce initial work without fragmenting the application into excessive requests and loading states.

  • Improved performance: By loading only the necessary code initially, you can reduce the initial load time of your application.
  • Less initial JavaScript: Route or feature chunks can keep unrelated code out of the initial download and execution path.
  • Tradeoff control: Choosing useful split points balances initial bundle size against extra requests and loading fallbacks.

Tools and libraries

Chunk creation and loading behavior ultimately depend on the application's bundler or framework:

  • Webpack / Rspack / Vite / Turbopack: These bundlers can create chunks at dynamic import() boundaries; exact chunking and preloading behavior depends on configuration and framework integration.
  • React Loadable: A predecessor to React.lazy that is now abandoned (unmaintained for years). New code should use lazy and Suspense instead.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

An admin editor is large and is opened by very few users. What is the most direct way to keep it out of the initial React bundle?

How would you optimize the performance of React contexts to reduce rerenders?

Topics
ReactPerformance

TL;DR

Context consumers re-render whenever the provider receives a different value according to Object.is. Neither memo nor React Compiler prevents that subscription update. Keep provider values stable, split unrelated or differently changing data into separate contexts, and separate state from a stable dispatch function so dispatch-only consumers do not re-render on state changes. Use memo to protect expensive descendants below a consumer, and consider selector-based state libraries when consumers need independent slices of frequently changing state.

const value = useMemo(() => ({ state, dispatch }), [state, dispatch]);

How to optimize the performance of React contexts to reduce rerenders

Context optimization starts by reducing unnecessary provider identity changes and narrowing which components subscribe to each value.

Know what React Compiler can and cannot optimize

React Compiler 1.0 is an optional build-time optimizer. When enabled, it can memoize components and values and reduce re-renders that come from unchanged props or parent renders. It can often replace hand-written memo, useMemo, and useCallback, but it does not change Context's subscription semantics.

Every component that reads a context re-renders when its closest provider receives a different value according to Object.is. The patterns below control when that value changes and reduce how many components subscribe to it. Profile before and after optimizing; a re-render that produces the same output is not automatically a performance problem.

Split state and dispatch into two contexts

A common effective pattern is splitting a context into two: one that exposes the frequently changing state and another that exposes the stable updater. Components that only need to dispatch do not receive a context update when state changes because the dispatch context's value remains stable.

import { createContext, useContext, useReducer } from 'react';
const StateContext = createContext(null);
const DispatchContext = createContext(null);
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
const initialState = { count: 0 };
export function CounterProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
// No need to memoize `dispatch` — useReducer guarantees it's stable.
// Memoize state only if you wrap it in an object.
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
}
export const useCounterState = () => useContext(StateContext);
export const useCounterDispatch = () => useContext(DispatchContext);

A button that only dispatches actions can call useCounterDispatch() without subscribing to StateContext, so a state update alone does not re-render it through context. It can still re-render for other reasons, such as its parent rendering or its own state changing.

Splitting the providers narrows the subscription fan-out when state changes:

Splitting state and dispatch context subscriptions

Memoize the value object you pass to a provider

If you must pass a single object value, memoize it so its reference is stable across renders. Note that hook return values like dispatch from useReducer and the setter from useState are already referentially stable — you don't need to include them in the dependency list.

import { createContext, useMemo } from 'react';
const MyContext = createContext(null);
function MyProvider({ state, children }) {
const value = useMemo(() => ({ state }), [state]);
return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}

Memoize expensive descendants of consumers

When a context value changes, memo cannot stop a component that reads that context from re-rendering. A consumer can instead pass a derived value to a memoized child, allowing that child to skip work when its props are unchanged:

import { memo, useContext } from 'react';
const Item = memo(function Item({ id }) {
return <ExpensiveItemDetails id={id} />;
});
function SelectedItem() {
const { selectedItem } = useContext(ItemsContext);
return <Item id={selectedItem.id} />;
}

This composes well with split contexts: a dispatch-only consumer receives no context update when state changes, while a state consumer can re-render without necessarily re-rendering memoized descendants.

Use the use(Context) API for conditional reads

React 19 added the use API, which can read a context inside conditionals or loops. Despite its name, use is not a Hook. It still subscribes the component to that context whenever the read occurs; conditional placement does not make updates cheaper by itself.

import { use } from 'react';
import { ThemeContext } from './theme-context';
function Avatar({ showTheme }) {
if (showTheme) {
const theme = use(ThemeContext);
return <img alt="Profile" className={`avatar avatar-${theme}`} />;
}
return <img alt="Profile" className="avatar" />;
}

Selectors with use-context-selector

For very large context values where consumers only care about a slice, use-context-selector lets a component subscribe to a derived value and only rerender when that derived value changes. Important: use-context-selector requires using its own createContext, not React's built-in one — selectors don't work with a context created by react.

import { createContext, useContextSelector } from 'use-context-selector';
const MyContext = createContext(null);
function Counter() {
// Only rerenders when `state.count` changes, even if other parts of the
// value change.
const count = useContextSelector(MyContext, (v) => v.state.count);
return <div>{count}</div>;
}

For frequently changing shared state with many independent slices, compare a dedicated state library such as Zustand, Jotai, or Redux Toolkit. Their selector-based subscriptions can narrow store-driven updates, at the cost of another abstraction and dependency.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A provider exposes { theme, cursorPosition, setTheme }. cursorPosition changes on every pointer move, causing theme-only consumers to render. Which structural change addresses the cause?

What are higher-order components in React?

Topics
React

TL;DR

Higher-order components (HOCs) in React are functions that take a component and return a new component with additional props or behavior. They are used to reuse component logic. For example, if you have a component MyComponent, you can create an HOC like this:

const withExtraProps = (WrappedComponent) => {
return (props) => <WrappedComponent {...props} extraProp="value" />;
};
const EnhancedComponent = withExtraProps(MyComponent);

HOCs were especially common before Hooks and remain valid in existing code and library APIs such as React Redux's connect. For new function components, custom Hooks usually share stateful logic with less wrapper nesting. An HOC can still be appropriate when the abstraction must return a component or wrap rendering rather than only share logic.


What are higher-order components in React?

HOCs are component-transforming functions: they accept one component type and return another that adds or coordinates behavior.

Definition

Higher-order components (HOCs) are functions in React that take a component as an argument and return a new component. The new component typically wraps the original component and adds additional props, state, or behavior. HOCs are a pattern for reusing component logic.

The current React learning material emphasizes custom Hooks for sharing stateful logic. HOCs are still a valid composition pattern, but they are most common in older codebases and library APIs that need to enhance or wrap a component.

Purpose

HOCs are used to:

  • Share common functionality between components
  • Abstract and reuse component logic
  • Enhance components with additional props or state

Example

Here is a simple example of an HOC that adds an extraProp to a wrapped component:

// Define the HOC
const withExtraProps = (WrappedComponent) => {
const ComponentWithExtraProps = (props) => {
return <WrappedComponent {...props} extraProp="value" />;
};
// Set a useful displayName for debugging in React DevTools
const wrappedName =
WrappedComponent.displayName || WrappedComponent.name || 'Component';
ComponentWithExtraProps.displayName = `withExtraProps(${wrappedName})`;
return ComponentWithExtraProps;
};
// Define a component to be wrapped
const MyComponent = (props) => {
return <div>{props.extraProp}</div>;
};
// Wrap the component using the HOC
const EnhancedComponent = withExtraProps(MyComponent);
// Use the enhanced component
const App = () => {
return <EnhancedComponent />;
};
export default App;

In this example, withExtraProps is an HOC that adds an extraProp to MyComponent. The EnhancedComponent now has access to extraProp. The displayName convention withExtraProps(MyComponent) makes the component identifiable in React DevTools.

Common use cases

HOCs remain most relevant where an API needs to wrap rendering or enhance a component type:

  • Authentication: Wrapping components to check if a user is authenticated before rendering.
  • Logging: Adding logging functionality to components.
  • Theming: Injecting theme-related props into components.
  • Data fetching: Fetching data and passing it as props to components.

Best practices

An HOC should preserve the wrapped component's identity contract and make its injected behavior explicit:

  • Do not mutate the original component: Always return a new component.
  • Do not create HOCs inside the render method: Calling withExtraProps(MyComponent) inside another component's render produces a brand-new component type on every render, so React unmounts and remounts the subtree, losing all state and DOM. Apply HOCs at the module level.
  • Set a displayName: Wrap the inner name as HOCName(WrappedName) so React DevTools shows something useful instead of Anonymous.
  • Watch for prop-name collisions: An HOC that injects, say, an extraProp will silently overwrite any extraProp the caller passes in (or vice versa, depending on spread order). Namespace injected props or document them clearly.
  • Forward refs explicitly: Wrapping a component in an HOC means a ref passed by the consumer lands on the wrapper, not the inner component. In React 19, forwardRef is no longer required and ref is now a regular prop on function components, so the simplest fix is to accept ref as a prop and forward it: <WrappedComponent {...props} ref={props.ref} extraProp="value" />. For pre-19 codebases, you'd use React.forwardRef inside the HOC.
  • Use HOCs sparingly: Overusing HOCs can make the code harder to understand and creates deeply nested wrapper trees ("wrapper hell"). Custom hooks usually compose more cleanly.

Alternatives

Modern React offers other reuse patterns that avoid adding a wrapper component when wrapping is unnecessary:

  • Custom hooks: Custom hooks share stateful logic between function components without adding wrapper components to the tree and are usually the simpler option for new code.
  • Render props: A pattern where a component uses a function as a prop to determine what to render.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

Which definition is a higher-order component?

What is the Flux pattern and what are its benefits?

Topics
React

TL;DR

Flux is a historical application architecture Facebook introduced for managing state around React. An action goes through a central dispatcher to registered stores; stores update their own domain state, emit changes, and views render the new state. This unidirectional flow makes the source of an update easier to trace and separates event creation, state transitions, and rendering. The original Flux library is archived, and modern applications usually choose reducers, external stores, or server-state caches according to their needs rather than implementing classic Flux.

  • Core components:
    • Dispatcher: Single hub that manages actions and dispatches them to all registered stores.
    • Stores: Hold the state and business logic; act as change emitters that notify subscribed views.
    • Actions: Plain payloads of information sent from the application to the dispatcher.
    • View: React components that subscribe to stores and re-render when stores emit changes.
  • Benefits:
    • Predictable state management due to unidirectional data flow.
    • Explicit ownership of each domain's state.
    • Improved debugging and testing.
    • Clear separation of concerns.

Example flow:

  1. User interacts with the View.
  2. Actions are triggered and dispatched by the Dispatcher.
  3. Stores process the actions, update their state, and emit a change event.
  4. View re-renders based on the updated state.

What is the Flux pattern?

Classic Flux coordinates application updates through actions, one dispatcher, multiple domain stores, and subscribed views.

Overview

Flux is a design pattern introduced by Facebook around 2014 to manage the flow of data in React applications. It enforces a unidirectional data flow, where data flows in one direction through specific components:

  1. Dispatcher: A single central hub that dispatches every action to all registered store callbacks.
  2. Stores: Manage the application's state and contain the business logic. Each store is a single source of truth for a slice of state and acts as a change emitter that views subscribe to.
  3. Actions: Plain objects representing the payloads of information sent to the dispatcher.
  4. View: React components that listen to stores for changes and re-render accordingly.

This structure simplifies state management, especially for complex applications, by ensuring data flows in a predictable and traceable manner.

Historical context

The original Flux library is archived, so it is primarily relevant when maintaining older code or discussing the history of React state management. Modern tools relate to Flux in different ways:

  • Redux preserves unidirectional action-driven updates but deliberately replaces Flux's multiple mutable stores with one store and pure reducers.
  • Zustand and Jotai are external state tools with different subscription models; they do not implement Flux's central dispatcher architecture.
  • useReducer + Context provides action/reducer-style updates with React APIs, but it is not the classic dispatcher-and-stores pattern.
  • TanStack Query and SWR manage cached server data. They solve a different problem from Flux's client application state.

Recoil is also no longer a current recommendation because its repository was archived in 2025. Understanding Flux remains useful for recognizing explicit actions and unidirectional updates, but those ideas do not make every modern state tool a Flux implementation.

Unidirectional data flow

Unlike traditional MVC patterns, where data can flow in multiple directions, Flux's unidirectional flow ensures consistency:

  1. User interactions trigger actions.
  2. Actions are sent to the dispatcher, which forwards them to stores.
  3. Stores update their state and notify the view to re-render.

The view can create another action, but state updates still travel through the dispatcher and stores in one direction:

Classic Flux unidirectional update cycle

Code example

This minimal store registers with the dispatcher before an action is sent:

import { Dispatcher } from 'flux';
const dispatcher = new Dispatcher();
// Store — register with the dispatcher before any actions are dispatched,
// otherwise the first dispatch will be a no-op for this store.
class CounterStore {
constructor() {
this.count = 0;
dispatcher.register((action) => {
if (action.type === 'INCREMENT') {
this.count += action.payload.amount;
console.log(`Count: ${this.count}`);
}
});
}
}
const store = new CounterStore();
// Action
const action = {
type: 'INCREMENT',
payload: { amount: 1 },
};
// Dispatching an action — the registered store callback runs now.
dispatcher.dispatch(action);

Benefits of the Flux pattern

Flux's benefits come from making the direction and participants of each state change explicit.

Predictable state management

The unidirectional data flow ensures that the application's state transitions are clear and predictable, making it easier to understand and debug.

Improved debugging and testing

Named actions and isolated store transitions make update paths easier to observe and exercise:

  • Each action represents a discrete event, making it easier to trace changes in the application.
  • Store callbacks centralize state transitions so they can be tested independently of the view. Classic Flux stores may mutate their internal state; purity is a Redux reducer convention, not a requirement of Flux.

Scalability

Domain stores and a shared dispatch protocol can impose consistent structure as an older Flux application grows:

  • As the application grows, the Flux pattern helps maintain a clear structure.
  • Decoupled components allow for modular development.

Clear separation of concerns

Each Flux participant has a distinct responsibility in the update cycle:

  • Actions encapsulate events and payloads.
  • Stores handle state and business logic.
  • Views focus on rendering the UI.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A view creates an ITEM_ADDED action in a classic Flux application. Which sequence assigns the update responsibilities correctly?

Explain one-way data flow of React and its benefits

Topics
React

TL;DR

In React, one-way data flow means that data moves from parent components to children through props and context. Children treat those inputs as read-only; to request a parent-state change, a child invokes a callback or dispatch function supplied from above. Controlled inputs may look like two-way binding, but they still update through an explicit event followed by a new value flowing down. The benefits are explicit ownership, predictable updates, and easier tracing and testing.


One-way data flow of React and its benefits

React makes state ownership explicit by separating values flowing down from update requests flowing back to their owner.

What is one-way data flow?

In React, one-way data flow refers to the concept where data moves in a single direction, from parent components to child components. This is achieved through the use of props. Parents pass data to children via props, and children may only read those props — they cannot mutate them. When a child needs to influence parent state, the parent passes down a callback (also via props) that the child invokes. The parent owns the state; the child requests changes.

This is sometimes called "unidirectional data flow" and contrasts with two-way data binding found in frameworks like Angular and Vue, where directives such as v-model keep an input element and a piece of state automatically in sync in both directions. React deliberately avoids this: the input is told what to display via a prop, and any change is reported back through an event handler.

Values and update requests take distinct paths, while the parent remains the owner that changes state:

React one-way data flow

Example

Here is a simple example to illustrate one-way data flow:

import { useState } from 'react';
function ChildComponent({ data, onChange }) {
return (
<div>
<p>{data}</p>
<button onClick={() => onChange('Hello from Child')}>Change data</button>
</div>
);
}
export default function ParentComponent() {
const [data, setData] = useState('Hello from Parent');
function handleChange(newData) {
setData(newData);
}
return (
<div>
<h1>{data}</h1>
<ChildComponent data={data} onChange={handleChange} />
</div>
);
}

In this example, ParentComponent passes data and a handleChange callback to ChildComponent via props. The child reads data and calls onChange to ask the parent to update its state. The child never writes to data directly. This is also a controlled component pattern: the parent is the single source of truth for the displayed value.

Lifting state up

When two sibling components need to share or coordinate state, the React idiom is to "lift state up" — move the state into their nearest common ancestor and pass it down via props, along with callbacks to update it. Because data only flows downward, the common ancestor becomes the natural owner of any state shared by its descendants. This keeps the source of truth explicit instead of trying to synchronize two independent copies.

Contrast with two-way binding

In two-way bound frameworks, an expression like Vue's <input v-model="name"> or Angular's [(ngModel)]="name" automatically updates the bound variable when the input changes, and updates the input when the variable changes. The same effect in React requires both halves to be wired explicitly:

<input value={name} onChange={(e) => setName(e.target.value)} />

This is more verbose, but every state change goes through a function you control, which makes the data flow easy to follow and intercept.

Benefits of one-way data flow

The pattern improves maintainability because each value has an identifiable owner and update path.

Predictable state changes

State only changes through explicit calls to setter functions in the component that owns it. You can read a component top-to-bottom and know exactly which props depend on which state, without worrying about a child silently mutating a parent's data.

Easier debugging

Because data flows downward and update requests flow upward through named callbacks or dispatches, you can trace a value from its owner to each consumer. React DevTools can inspect props and state along that tree. State libraries can additionally provide replayable histories when their updates are represented as immutable transitions.

Encourages immutable updates

Children receive props as read-only inputs, and the recommended way to update state is to produce a new value rather than mutate the existing one. This pairs well with React.memo, useMemo, and the React Compiler, which can rely on referential equality to skip unnecessary work.

Reusable, testable components

A component that only depends on its props and never reaches out to mutate parent state is easy to reuse in different parents and easy to test by passing in fixture props.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which statements correctly describe one-way data flow in React? Select all that apply.

How do you handle asynchronous data loading in React applications?

Topics
ReactAsync

TL;DR

Prefer a framework's data-loading APIs when they can coordinate route requests before rendering, or a client cache such as TanStack Query, SWR, or RTK Query when browser-owned data needs caching and synchronization. Exact deduplication, retries, and refetch behavior depend on the tool and configuration. Fetching in an Effect remains valid for simple client-only cases, but it needs explicit loading/error handling, cleanup, and stale-response protection. React 19's use API can read a cached promise and suspend; the promise should come from a Suspense-enabled framework/cache, a route loader, or a Server Component rather than being recreated during render.


Handling asynchronous data loading in React

The right loading layer depends on where the data is needed, who owns its cache, and whether it participates in server rendering or client interactions.

Prefer a framework or data-fetching library for production applications

The React docs recommend considering a framework's built-in data fetching or a client-side cache because those layers can handle caching, deduplication, refetching, race conditions, retries, and loading/error states. Fetching in an Effect is still supported when those alternatives do not fit.

TanStack Query is one popular option for client-side fetching:

import { useQuery } from '@tanstack/react-query';
function Profile({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: async ({ signal }) => {
const res = await fetch(`/api/users/${userId}`, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <h1>{data.name}</h1>;
}

SWR is a lightweight alternative with a similar mental model. RTK Query is the right pick if you already use Redux Toolkit.

Depending on the library and its configuration, these tools can provide:

  • A cache keyed by query parameters.
  • Deduplication of matching in-flight requests.
  • Configurable refetching on focus, reconnect, or an interval.
  • AbortSignal integration and configurable retries.
  • Primitives for pagination, infinite queries, and optimistic updates.

Server Components and framework loaders

When data is needed for the initial route and does not depend on browser-only state, a framework's server loader can avoid a client request waterfall and keep fetching code out of the client bundle. Client caches remain appropriate for highly interactive, frequently refreshed, or browser-specific data.

  • Next.js App Router (React Server Components) — async Server Components can await data directly and stream the result.
  • React Router / Remix loaders — co-locate a loader with the route; data is fetched in parallel with code.
// Next.js Server Component (no useEffect needed)
async function UserPage({ params }) {
const { id } = await params;
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const user = await response.json();
return <h1>{user.name}</h1>;
}

React 19: use() + <Suspense>

React 19's use() API lets a Client Component read a cached promise. While the promise is pending, the component suspends and the nearest <Suspense> boundary shows the fallback; if the promise rejects, the nearest error boundary catches it. Create or cache the promise outside the rendering Client Component—often in a route loader, Suspense-enabled data source, or Server Component—and pass it down so the same promise is reused across render attempts.

'use client';
import { use, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function UserName({ userPromise }) {
const user = use(userPromise); // suspends until resolved
return <h1>{user.name}</h1>;
}
function Page({ userPromise }) {
return (
<ErrorBoundary fallback={<div>Failed to load</div>}>
<Suspense fallback={<div>Loading...</div>}>
<UserName userPromise={userPromise} />
</Suspense>
</ErrorBoundary>
);
}

Keeping the UI responsive: useTransition and useDeferredValue

When an interaction changes a Suspense-enabled result, marking the state update as a Transition can keep already revealed content visible and interactive while the next result loads. useDeferredValue provides a lagging value that can let urgent input updates render before slower derived content. Neither API fetches or caches data by itself.

const [isPending, startTransition] = useTransition();
function onTabChange(next) {
startTransition(() => setTab(next));
}

Low-level: useEffect + fetch (when and how)

Plain useEffect + fetch is a valid fallback for a one-off client request. A complete implementation must handle two details that short examples commonly omit: race conditions (an older request resolving after a newer one) and HTTP errors (fetch only rejects on network failure — a 500 still resolves).

import { useEffect, useState } from 'react';
function User({ id }) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
(async () => {
try {
const res = await fetch(`/api/users/${id}`, {
signal: controller.signal,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
setData(json);
} catch (err) {
if (err.name !== 'AbortError') setError(err);
} finally {
// Do not let an aborted, stale request clear a newer request's loading state.
if (!controller.signal.aborted) setLoading(false);
}
})();
// Cancel the in-flight request if `id` changes or the component unmounts
return () => controller.abort();
}, [id]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <h1>{data.name}</h1>;
}

Even with these fixes, useEffect alone does not provide cross-component caching, request deduplication, or refetch-on-focus. Add those capabilities only when the application needs them, usually through a framework loader or data library.

Summary of options

Use this table as a starting point, then account for the framework and freshness requirements of the specific data:

OptionUse when
TanStack Query / SWR / RTK QueryShared client-side data that benefits from caching and synchronization
Server Components / route loadersYou control the framework (Next.js, Remix)
use(promise) + <Suspense>Streaming a promise from a parent in React 19+
useEffect + fetchSimple client-only fetches where you handle cleanup and races

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 4
Check your understanding Exercise 1 of 4

A client-only search Effect fetches on every query change. A slow response for an old query sometimes replaces a newer result. What directly addresses the race?

Explain server-side rendering of React applications and its benefits

Topics
React

TL;DR

Server-side rendering (SSR) renders React output to HTML on the server and sends it to the browser. hydrateRoot then attaches React to matching client-rendered output so interactive Client Components can work. Modern React streams with renderToPipeableStream for Node streams or renderToReadableStream for Web Streams. React 19.2 supports Web Streams in Node too, although the React team recommends Node streams there for performance. Benefits include earlier content and crawlable HTML; tradeoffs include server work, hydration cost, and mismatch risk.


What is server-side rendering of React applications?

SSR combines server-produced HTML with client hydration; understanding both phases is necessary to evaluate its benefits and costs.

Definition

Server-side rendering (SSR) is a technique where the server renders the initial HTML of a React application and sends it to the client. This is in contrast to client-side rendering (CSR), where the browser downloads a minimal HTML page and renders the content using JavaScript.

How it works

An SSR request moves through server rendering, delivery, and client hydration in this order:

  1. Initial request: When a user requests a page, the server processes the request.
  2. Rendering on the server: The server uses React's server APIs (renderToPipeableStream on Node, renderToReadableStream on Web/edge runtimes) to render components into HTML.
  3. Sending HTML to the client: The server streams the HTML to the client. With streaming SSR, the browser can start parsing and painting before the whole page is ready.
  4. Hydration: Once the JavaScript bundle loads, the client calls hydrateRoot to attach event handlers to the existing DOM and resume React on the client. With selective hydration, React can hydrate parts of the tree as they become ready and prioritize the part the user is interacting with.

SSR performs server work for an incoming request, then hands the existing DOM to the client rather than rendering a second, unrelated page over it:

Server-side rendering request lifecycle

Streaming SSR and React Server Components

Modern React provides two primary streaming server APIs:

  • renderToPipeableStream for Node.js streams.
  • renderToReadableStream for Web Streams. React 19.2 also supports it in Node, but renderToPipeableStream is recommended for Node because Node streams are faster and support compression more naturally.

Both let you wrap parts of the tree in <Suspense> so the server can flush the shell first and stream slower parts as their data resolves. React 19.2 briefly batches nearby server-rendered boundary reveals so content can appear together, using heuristics to avoid delaying important loading metrics.

React Server Components (RSC) are a separate architecture from SSR. Their component code runs on the server and is not included in the client bundle, although their rendered output can be part of the response. Modules that need state, effects, or browser APIs use the 'use client' directive to define a client boundary. Frameworks such as Next.js App Router integrate RSC with SSR and streaming.

Code example

Here is a basic example using Next.js's App Router, which uses async server components by default:

// app/page.jsx — a Server Component (no 'use client' directive)
async function fetchDataFromAPI() {
const res = await fetch('https://api.example.com/data', {
// Opt into a request-time fetch for this example.
cache: 'no-store',
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export default async function Home() {
const data = await fetchDataFromAPI();
return (
<div>
<h1>Welcome to my SSR React app</h1>
<p>Data from server: {data.message}</p>
</div>
);
}

Any interactive piece (e.g. a button with onClick) would live in a separate file that starts with 'use client'.

Hydration cost and mismatches

Hydration is not free. The browser has to download, parse, and execute the JS bundle, then walk the DOM and attach handlers. For large apps this can delay Time To Interactive even though pixels appeared quickly.

Hydration mismatches occur when server HTML differs from the client's initial render—for example, rendering time or randomness independently on both sides, or branching on window. React 19 reports a consolidated diff and may regenerate a mismatched tree on the client. The fix is to render deterministic initial content, pass the server value to the client, or intentionally switch browser-only content after hydration.

Benefits of server-side rendering

SSR is most useful when meaningful response HTML improves delivery or crawlability enough to justify request-time rendering and hydration.

Improved initial load time

The timing benefit depends on server latency, streaming, caching, bundle size, and the equivalent client-rendered path.

  • Earlier content under the right conditions: A nearby, efficiently cached or streaming server can deliver meaningful HTML before a client-rendered application finishes downloading and executing its JavaScript. SSR is not automatically faster than static HTML or every CSR application, so measure the actual route.

Better SEO

SSR affects whether content is present in the initial response, not the full set of signals used for search ranking.

  • Crawlable response content: Crawlers can read the server-rendered content without waiting to execute application JavaScript. That improves content availability to crawlers but does not by itself guarantee higher search rankings.

Performance on slower devices

Server-rendered HTML can move initial document construction away from the browser, but hydration still consumes client resources.

  • HTML construction before JavaScript: SSR lets a low-powered device display server-produced HTML before hydration. Traditional SSR still sends component JavaScript and performs client rendering during hydration; React Server Components can additionally reduce the JavaScript sent for server-only components.

Tradeoffs to be aware of

SSR changes where and when work happens rather than removing that work entirely.

  • Higher TTFB: Time To First Byte goes up because the server has to render before responding. Streaming SSR mitigates this by flushing the shell early, but the server is still doing work CSR pushes to the client.
  • Hydration cost: Interactive readiness is bounded by how fast the JS bundle downloads, parses, and hydrates. Big bundles delay TTI even when pixels appear quickly.
  • Hydration mismatches: Server and client output must agree, which constrains how you read time, randomness, and browser-only APIs during render.
  • Server cost and complexity: You need a Node/edge runtime to render on every request, plus caching strategy for hot pages. Static generation or ISR may be a better fit for content that does not change per request.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

Which sequence best describes a server-rendered interactive React page?

Explain static generation of React applications and its benefits

Topics
React

TL;DR

Static generation (SSG) renders HTML ahead of requests, usually during a build, so the result can be cached and served from a CDN. React itself provides static rendering APIs, while frameworks decide how routes, data caches, and revalidation work. In Next.js, generateStaticParams can enumerate dynamic routes to prerender, but current caching is explicit rather than every fetch being static by default. SSG is best for content that does not vary per request; framework features such as revalidation and partial prerendering can combine cached shells with fresher content.


Static generation of React applications and its benefits

Static generation moves rendering ahead of incoming requests so the same output can be distributed and reused efficiently.

What is static generation?

Static generation is a method of pre-rendering where the HTML of a page is generated at build time. This means that the HTML is created once, during the build process, and then reused for each request. In the context of React applications, this is often achieved using frameworks like Next.js.

How does static generation work?

A typical static-generation pipeline performs these stages before and during delivery:

  1. Build time rendering: During the build process, the framework generates the HTML for each page based on the React components and data.
  2. Static files: The generated HTML, CSS, and JavaScript files are then stored as static files.
  3. Serving the files: These static files can be served directly from a CDN or a web server, without the need for server-side rendering on each request.

The expensive rendering step happens before traffic arrives, so many requests can reuse the same deployed output:

Static generation build and delivery lifecycle

Benefits of static generation

The strongest benefits come from reusing pre-rendered output rather than executing React for every request.

Improved performance

Pre-rendering removes request-time rendering from the critical path, subject to normal network and client costs.

  • Low and predictable server latency: Pre-generated HTML can be served from a nearby CDN without request-time React rendering. Actual load time still depends on document size, network conditions, and client assets.
  • Reduced server load: Static files can be served from a CDN, reducing the load on the origin server.
Better SEO

Static output exposes the page's content directly in the response that crawlers receive.

  • Search engine indexing: Pre-rendered content is present in the response, so crawlers do not need to execute application JavaScript to discover it. This helps crawlability but does not guarantee search ranking.
  • Consistent content: Every request returns the same HTML, so what crawlers see matches what users see.
Scalability

Serving immutable artifacts lets cache infrastructure absorb traffic without repeating application rendering.

  • CDN distribution: Static files can be replicated across CDN edges, so traffic spikes are absorbed at the edge instead of hammering an origin.
  • Efficient caching: Static files have stable URLs and content hashes, which makes long-lived browser and CDN caching straightforward.

Example with Next.js (App Router)

In the Next.js App Router, generateStaticParams enumerates dynamic route parameters to prerender at build time. Data that should be reused must opt into the relevant cache model—for example, cache: 'force-cache' in the previous caching model or use cache with Cache Components in Next.js 16. Server Components are not inherently static; request-time data can make a route dynamic.

// app/posts/[slug]/page.jsx
async function getPost(slug) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
cache: 'force-cache',
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export async function generateStaticParams() {
const response = await fetch('https://api.example.com/posts');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const posts = await response.json();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}

At build time Next.js calls generateStaticParams to learn the list of slugs, renders each PostPage to HTML, and ships the result as static files.

Revalidation and partial prerendering

Frameworks can refresh cached output without rebuilding the entire site. In Next.js's previous caching model, ISR uses route- or fetch-level revalidation. With Next.js 16 Cache Components, caching and lifetimes are expressed with use cache and cacheLife, and Suspense boundaries can defer request-time content outside the prerendered shell.

// app/posts/[slug]/page.jsx
export const revalidate = 60; // seconds
export default async function PostPage({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.title}</article>;
}

This revalidate example applies when Cache Components are disabled. Current Next.js also supports on-demand invalidation and Cache Components APIs; the exact semantics are framework- and version-specific rather than part of React itself.

When SSG is not a good fit

Prefer request-time or client-side data for content whose identity, freshness, or route space cannot be determined safely ahead of time.

  • Per-user content: Anything personalized (auth-gated dashboards, "hello {user}", carts) cannot be pre-rendered for every visitor — use SSR or client-side fetching for the personalized parts.
  • Highly dynamic data: Live prices, stock levels, scoreboards, etc. need fresher data than even ISR comfortably provides.
  • Huge or unbounded route spaces: If a framework tries to emit millions of pages, build time and storage become a problem. Prerender a subset, generate routes on demand, or use a partial/static shell with request-time content.
  • Stale-data tradeoffs: Plain SSG serves whatever was true at build time until the next deploy. Stale-while-revalidate ISR modes shorten that window but may serve the previous version while regeneration runs; exact behavior depends on the framework and invalidation method.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which route is a good default candidate for static generation?

Explain the presentational vs container component pattern in React

Topics
React

TL;DR

The presentational vs container component pattern (also known as "dumb vs smart components") splits components into two roles: presentational components decide how things look and receive everything via props, while container components decide how things work — they fetch data, hold state, and pass props down. Dan Abramov, who popularized the pattern in 2015, updated his original article in 2019 to say he no longer recommends splitting components this way: hooks (especially custom hooks) cover the same separation of concerns without forcing you to introduce a wrapper component. The vocabulary is still useful for talking about responsibilities, but in modern React the "container" layer is usually a custom hook.


Presentational vs container component pattern in React

The pattern is best understood as a historical way to separate rendering from stateful orchestration, with custom Hooks now covering much of the orchestration role.

A note on relevance

This pattern was widely used in the class-component era (2015–2018) and is still common in older codebases — particularly Redux apps that lean on connect. Since hooks landed in React 16.8 (2019), the standard way to encapsulate data-fetching and stateful logic is a custom hook, not a wrapper component. Dan Abramov, who introduced and popularized the pattern, edited his original article to recommend hooks instead.

It is still worth knowing the pattern: the underlying idea — separating "how it looks" from "how it works" — is sound, and a lot of code in the wild is structured this way.

Presentational components

Presentational components are concerned with the UI. They receive data and callbacks exclusively via props and rarely own state beyond local UI state (e.g. whether a dropdown is open, whether a tooltip is visible, the current value of an uncontrolled input). They are written as function components.

Characteristics

Presentational components are identified by where their data and behavior come from:

  • Focus on how things look
  • Receive data and callbacks via props
  • Rarely own state beyond local UI state
  • Written as function components
  • Do not subscribe directly to external stores (Redux, Zustand, etc.) — they receive the data they need via props
Example

This button delegates behavior to its caller and only owns the rendered markup:

const Button = ({ onClick, label }) => (
<button onClick={onClick}>{label}</button>
);

Container components

Container components are concerned with how things work. They fetch data, manage state, and pass that data down to presentational components as props.

Characteristics

Container components coordinate the data sources and callbacks needed by their presentational children:

  • Focus on how things work
  • Manage state and business logic
  • Fetch data and handle user interactions
  • Pass data and callbacks to presentational components
  • May subscribe to a store (Redux, Zustand, etc.) or call a data-fetching library
Class container example (legacy)

The original 2015 form looked like this — a class component, often connected to a Redux store, that wrapped a presentational component:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchData } from './actions';
import Button from './Button';
class ButtonContainer extends Component {
componentDidMount() {
this.props.fetchData();
}
handleClick = () => {
this.props.fetchData();
};
render() {
return <Button onClick={this.handleClick} label="Click me" />;
}
}
const mapDispatchToProps = {
fetchData,
};
export default connect(null, mapDispatchToProps)(ButtonContainer);
Modern equivalent with a custom hook

In modern React, the same separation is expressed by extracting the logic into a custom hook and keeping a single function component. The presentational component (Button) does not change.

import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { fetchData } from './actions';
import Button from './Button';
function useButtonContainer() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchData());
}, [dispatch]);
function handleClick() {
dispatch(fetchData());
}
return { handleClick };
}
export default function ButtonContainer() {
const { handleClick } = useButtonContainer();
return <Button onClick={handleClick} label="Click me" />;
}

The custom hook is the "container" — it owns the data and behavior. The component that calls it is just a thin glue layer, and the presentational Button stays pure.

Benefits

Separating rendering from orchestration can make each concern easier to reuse and test when the boundary is natural.

  • Separation of concerns: By separating the UI from the logic, the codebase becomes more modular and easier to maintain.
  • Reusability: Presentational components can be reused across different parts of the application since they are not tied to specific logic.
  • Testability: Presentational components are easy to test because they are pure functions of their props; custom hooks can be tested in isolation with @testing-library/react's renderHook.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A reusable OrdersTable contains a live subscription, converts incoming records into rows, and renders cells. Another screen needs the same live rows in a chart. Which extraction lets both views reuse the data behavior?

What are some common pitfalls when doing data fetching in React?

Topics
React

TL;DR

Common data-fetching pitfalls include missing loading and error states, ignoring HTTP error statuses, leaving stale requests active, allowing responses to win races, recreating promises during render, and triggering request waterfalls. Prefer framework loaders or a client cache when they fit. For manual Effect-based fetching, clean up with AbortController and handle Strict Mode's extra development setup/cleanup cycle. React 19's use API can read a cached promise with Suspense, but it does not make an ordinary fetch() created during render safe.


Common pitfalls when doing data fetching in React

Reliable data loading must coordinate request lifetime, response ordering, HTTP semantics, rendering state, and cache ownership.

Not handling loading and error states

When fetching data, it's crucial to manage the different states of the request: loading, success, and error. Failing to do so can lead to a poor user experience.

import { useEffect, useState } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch('https://api.example.com/data', { signal: controller.signal })
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then((data) => {
setData(data);
setLoading(false);
})
.catch((error) => {
if (error.name === 'AbortError') return;
setError(error);
setLoading(false);
});
return () => controller.abort();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}

Not aborting in-flight requests on unmount

If a component unmounts (or its effect re-runs) before a fetch resolves, calling setState afterwards is wasted work and previously triggered React's "state update on unmounted component" warning. Since React 18, the recommended fix is AbortController — it both stops the network request and prevents the resolve handler from running.

import { useEffect, useState } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
let ignore = false;
fetch('https://api.example.com/data', { signal: controller.signal })
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then((data) => {
if (!ignore) setData(data);
})
.catch((error) => {
if (!ignore && error.name !== 'AbortError') setError(error);
});
return () => {
ignore = true;
controller.abort();
};
}, []);
if (error) return <p role="alert">{error.message}</p>;
if (data === null) return <p>Loading...</p>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

An Effect-local ignore flag can prevent a stale response from committing state and therefore can handle races, including for asynchronous APIs that cannot be canceled. It does not stop the underlying network request. For fetch, prefer AbortController so cleanup both prevents the stale commit and cancels the request.

Race conditions when props or query params change

If a fetch depends on a prop (such as a user ID) and the prop changes before the previous request resolves, responses can arrive out of order and the stale one can overwrite the fresh one. Cleanup can abort the superseded fetch, while an ignore flag prevents its handlers from committing stale state:

Preventing an out-of-order response from winning a race
import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
let ignore = false;
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then((data) => {
if (!ignore) setUser(data);
})
.catch((error) => {
if (!ignore && error.name !== 'AbortError') console.error(error);
});
return () => {
ignore = true;
controller.abort();
};
}, [userId]);
return user === null ? <p>Loading...</p> : <h1>{user.name}</h1>;
}

Forgetting Strict Mode's extra development checks

In development with <StrictMode>, React performs an extra Effect setup-and-cleanup cycle before the real setup. If a fetch or subscription appears duplicated, treat it as a signal that the Effect needs correct cleanup. Mutations such as analytics events or POST requests usually belong in the user event that caused them, not in an Effect that runs because a component appeared.

Creating an uncached request during render

Calling fetch directly in a Client Component's render body creates a new promise on every attempt. If its handler sets state, it also creates a render loop. Suspense does support reading promises during render through use, but the promise must be cached so the same instance is reused.

// Incorrect — fetch runs every render
function MyComponent() {
const [data, setData] = useState(null);
// This fires a request on every render. If `setData` is called inside,
// each render schedules another render → infinite loop.
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => setData(data));
return <div>{JSON.stringify(data)}</div>;
}
// Correct — run the side effect inside useEffect and clean it up
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch('https://api.example.com/data', { signal: controller.signal })
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then((data) => setData(data))
.catch((error) => {
if (error.name !== 'AbortError') console.error(error);
});
return () => controller.abort();
}, []);
return <div>{JSON.stringify(data)}</div>;
}

Missing or incorrect dependencies in useEffect

The dependency array tells React when to re-run an effect. Omitting a value the effect actually reads (such as userId) leaves you with stale data. Including a value that changes on every render (such as a freshly created object or function) will re-fire the effect on every render, which can create a fetch loop. This abbreviated comparison isolates the dependency mistake; a complete request still needs the HTTP checks, error state, and cleanup shown above.

// Incorrect — uses `userId` but doesn't list it; data goes stale.
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((r) => r.json())
.then(setUser);
}, []);
// Correct dependency list (request handling abbreviated)
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((r) => r.json())
.then(setUser);
}, [userId]);

Let eslint-plugin-react-hooks (exhaustive-deps) catch these for you.

Request waterfalls

Fetching one resource, waiting for it, then fetching the next from a child component creates a serial waterfall. If the requests don't depend on each other, kick them off in parallel with Promise.all, or hoist them to a parent / loader / Server Component so they start at the same time.

Skipping caching, deduplication, and retries

Hand-rolled useEffect + fetch has no cache, no deduping, no retries, no background refetching, no stale-while-revalidate. For anything beyond a one-off request, reach for a dedicated library:

  • TanStack Query (@tanstack/react-query) — caching, dedup, retries, background refresh.
  • SWR — small, focused on stale-while-revalidate.
  • RTK Query — built on Redux Toolkit, good fit if you already use Redux.

Misusing React 19's use() and Suspense

In React 19, use() can read a cached promise and let Suspense handle the pending state. The promise must be reused across renders and should usually come from a Server Component, route loader, or Suspense-enabled cache. Recreating fetch() inside the component will repeatedly suspend instead of completing:

import { use, Suspense } from 'react';
function User({ userPromise }) {
const user = use(userPromise); // suspends until the promise resolves
return <div>{user.name}</div>;
}
function App({ userPromise }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<User userPromise={userPromise} />
</Suspense>
);
}

Choose the loading layer based on the application: Server Components or route loaders avoid client waterfalls when the framework supports them, while a client cache is appropriate for browser-owned server state and background refetching. Effect-based fetching remains a low-level option for simpler cases.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 4
Check your understanding Exercise 1 of 4

A profile request uses this code. The UI displays data when set and has no other error handling. Which changes are needed to distinguish a usable profile from a failed request? Select all that apply.

fetch('/api/profile')
.then((response) => response.json())
.then(setData)
.catch(console.error);

What are render props in React and what are they for?

Topics
React

TL;DR

Render props in React are a technique for sharing code between components using a prop whose value is a function. The component calls that function with some internal state or data, and the function returns the React element to render. The prop does not have to be named render — passing a function as children is the more common modern form.

import { useState } from 'react';
function Toggle({ children }) {
const [on, setOn] = useState(false);
return (
<>
<button onClick={() => setOn((value) => !value)}>Toggle</button>
{children(on)}
</>
);
}
<Toggle>{(on) => <p>{on ? 'On' : 'Off'}</p>}</Toggle>;

Render props were popular before hooks. As of modern React, custom hooks have largely replaced them for sharing stateful logic, though render props are still useful for components that own a piece of UI structure (e.g. virtualized lists, headless component libraries).


What are render props in React and what are they for?

The render-prop pattern shares behavior by letting a component call a function supplied by its consumer to produce UI.

Definition

Render props is a pattern in React for sharing code between components by passing a function as a prop. The component invokes that function with some piece of state or data and renders whatever it returns. The pattern gets its name from the prop, which is conventionally called render — though children as a function is just as common, and any prop name works.

Purpose

Render props are used to:

  • Share logic between components without using higher-order components (HOCs)
  • Make components more reusable and composable
  • Improve code readability and maintainability

In modern React, custom hooks are the preferred way to share stateful logic between function components. Render props remain a good fit when the shared component also needs to own some piece of UI structure (e.g. wrapping children in an event listener container, virtualized lists, or "headless" UI libraries).

How it works

A component that uses a render prop takes a function as a prop. The component calls this function during render with whatever state or data it wants to expose, and renders the React element that the function returns.

Example

Here is a simple example using a function component and hooks:

import { useState } from 'react';
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
}
// Usage
<MouseTracker
render={({ x, y }) => (
<h1>
The mouse position is ({x}, {y})
</h1>
)}
/>;

In this example, MouseTracker tracks the mouse position and passes the coordinates to the render prop function. The render prop function then determines how to display the coordinates.

children as a function variant

A widely used variation passes the function as children instead of a named render prop. It reads more naturally because the consumer's UI lives between the component's tags:

<MouseTracker>
{({ x, y }) => (
<h1>
The mouse position is ({x}, {y})
</h1>
)}
</MouseTracker>

The component's implementation just calls children(...) instead of render(...):

return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{children(position)}
</div>
);

Performance caveat

Defining the render prop inline (which is the usual style) creates a new function on every render of the parent. That new function reference defeats React.memo on the render-prop component itself, because the render (or children) prop is never referentially equal between renders. If the wrapping component is expensive to re-render, you can wrap the function in useCallback, hoist it to module scope, or — more commonly — switch to a custom hook, which sidesteps the issue entirely.

Benefits

The pattern separates reusable stateful behavior from the markup chosen by each consumer.

  • Reusability: The logic for tracking the mouse position is encapsulated in MouseTracker, making it reusable across different parts of the application.
  • Separation of concerns: The MouseTracker component is responsible for tracking the mouse position, while the render-prop function is responsible for rendering the UI.
  • Flexibility: Different UI representations can be created by passing different functions to the same MouseTracker component.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A MouseTracker owns pointer state. Which use of a caller-supplied function is the render-prop pattern?

What are some React anti-patterns?

Topics
React

TL;DR

React anti-patterns are practices that lead to inefficient, buggy, or hard-to-maintain code. Common ones in modern (hooks-era) React include:

  • Mutating state directly instead of producing a new value
  • Using useState to mirror props or other state instead of computing the value during render
  • Using useEffect to derive data that could just be computed
  • Using array index as key for dynamic lists
  • Stale closures inside effects (missing or wrong dependencies)
  • Forgetting to clean up effects (subscriptions, timers, listeners)
  • Mutating refs during render
  • Not using keys in lists at all
  • Reaching for useMemo/useCallback everywhere instead of where they actually help

Common React anti-patterns

Most React anti-patterns violate one of three ideas: renders should be pure, state should have one owner, and identity should remain stable for the same logical item.

Mutating state directly

Directly mutating state breaks React's snapshot model. Passing the same object back also makes the update eligible for an Object.is bailout, so React may not re-render for the mutation. Always produce a new value and pass it to the setter.

// Anti-pattern
const [user, setUser] = useState({ name: 'Ada', age: 36 });
user.age = 37; // mutation — React doesn't see this
setUser(user); // same reference; React can ignore this update
// Correct
setUser((prev) => ({ ...prev, age: 37 }));

Using state to mirror props or other state

A common anti-pattern is copying a prop into state in order to "have a local copy." This usually leads to two sources of truth that drift out of sync.

// Anti-pattern — `fullName` mirrors props in state
function Greeting({ firstName, lastName }) {
const [fullName, setFullName] = useState(`${firstName} ${lastName}`);
// fullName won't update when firstName/lastName change!
return <h1>{fullName}</h1>;
}
// Correct — compute it during render
function Greeting({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`;
return <h1>{fullName}</h1>;
}

If the derived value is genuinely expensive, measure it and consider useMemo. React Compiler may provide equivalent memoization when enabled.

Using useEffect to derive data

If a value can be computed from existing props/state, don't store it in state and update it from an effect — just compute it during render. The effect version adds an extra render, can flash stale values, and is harder to reason about.

// Anti-pattern
function Cart({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price, 0));
}, [items]);
return <div>{total}</div>;
}
// Correct
function Cart({ items }) {
const total = items.reduce((sum, i) => sum + i.price, 0);
return <div>{total}</div>;
}

The React docs have a full guide on this: You Might Not Need an Effect.

Using array index as key on dynamic lists

key={index} is fine if the list is static and never reordered. As soon as items can be inserted, removed, or reordered, index keys cause React to reuse the wrong DOM nodes and component state — leading to subtle bugs (text inputs keeping the wrong value, animations playing on the wrong row).

// Anti-pattern for a list that can change
items.map((item, i) => <Row key={i} item={item} />);
// Correct
items.map((item) => <Row key={item.id} item={item} />);

Not using keys in lists at all

Omitting key triggers a console warning and forces React to fall back to position-based reconciliation, which is the same problem as index keys. Always use a stable, unique key.

// Anti-pattern
items.map((item) => <li>{item.name}</li>);
// Correct
items.map((item) => <li key={item.id}>{item.name}</li>);

Stale closures in effects

When an effect captures a value but doesn't list it in the dependency array, it keeps reading the old value forever. This shows up as "the timer keeps logging 0," "the WebSocket sends old form values," etc.

// Anti-pattern — effect closes over `count` but doesn't depend on it
useEffect(() => {
const id = setInterval(() => {
console.log(count); // always logs the initial value
}, 1000);
return () => clearInterval(id);
}, []);
// Correct — re-synchronize the interval when `count` changes
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, [count]);

Let eslint-plugin-react-hooks's exhaustive-deps rule catch these.

Forgetting to clean up effects

Subscriptions, timers, intervals, and event listeners need to be torn down in the Effect's cleanup function. Otherwise they leak—and at a Strict Mode root, the extra development setup/cleanup cycle makes missing cleanup easier to notice.

// Anti-pattern
useEffect(() => {
window.addEventListener('resize', onResize);
}, []);
// Correct
useEffect(() => {
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);

Mutating refs during render

Reading or writing ref.current during render, except for predictable one-time initialization, breaks render purity and can make component behavior unpredictable. The current eslint-plugin-react-hooks refs rule reports many such cases. Read or write refs in event handlers and Effects instead.

// Anti-pattern
function Component() {
const ref = useRef(0);
ref.current += 1; // side effect during render
return <div>{ref.current}</div>;
}
// Correct
function Component() {
const clickCount = useRef(0);
function handleClick() {
clickCount.current += 1;
alert(`Recorded clicks: ${clickCount.current}`);
}
return <button onClick={handleClick}>Record click</button>;
}

Inline functions and objects in JSX

Defining a function or object inline (onClick={() => doThing(id)}, style={{ color: 'red' }}) creates a fresh reference each render. This is not automatically a performance problem — for the vast majority of components it's the idiomatic style, and the React docs explicitly say not to optimize prematurely. It matters when identity affects a memoized child or a Hook dependency. React Compiler can memoize many compatible cases when enabled.

Prop drilling and the over-correction into context

Passing a prop through five layers that don't use it is annoying, but the fix isn't always context. Often the right answer is to lift state to the right place, compose components differently (children, slots), or pull in a state library for genuinely global state. Wrapping every shared value in context invites the context pitfalls — every consumer re-renders on every change.

Overusing useMemo and useCallback

Memoization isn't free — it costs memory plus the equality check on every render. Sprinkling useMemo/useCallback on every value "just in case" usually loses time rather than saving it. Reach for them when:

  • The wrapped computation is genuinely expensive, or
  • The value is passed to a React.memo'd child or a hook dependency array where reference stability matters.

React Compiler 1.0 is an optional build-time optimizer that memoizes compatible code automatically. It can reduce manual memoization, but profiling and clear dependency semantics still matter.

Deeply nested state

Deeply nested state is awkward to update immutably and easy to get wrong. Prefer a flatter shape, but don't lose information that the structure encoded. The fix below loses the "users have profiles" grouping; a better fix keeps the relationship while flattening the tree:

// Anti-pattern — deeply nested
const [state, setState] = useState({
user: {
profile: {
name: 'John',
age: 30,
},
},
});
// Correct — flatten while preserving structure
const [user, setUser] = useState({ name: 'John', age: 30 });
// Or, for collections, normalize by id
const [users, setUsers] = useState({
byId: {
u1: { id: 'u1', name: 'John', age: 30 },
},
allIds: ['u1'],
});

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 3
Check your understanding Exercise 1 of 3

Which implementations are React anti-patterns? Select all that apply.

How do you decide between using React state, context, and external state managers?

Topics
React

TL;DR

Match the tool to the kind of state. Use useState/useReducer for local component state, and lift state up before reaching for anything heavier. Context transports a value through a subtree; it does not provide storage, update logic, or fine-grained subscriptions by itself. Reach for a client-state library such as Zustand, Jotai, or Redux Toolkit when many unrelated components need shared state and selector-based subscriptions or store tooling. Treat server state separately: a framework data layer or a library such as TanStack Query, SWR, or RTK Query can add caching, refetching, and invalidation when the application needs them.


Deciding between React state, context, and external state managers

Choose the smallest state mechanism that matches the value's ownership, update frequency, subscriber shape, and persistence needs.

React state (useState and useReducer)

React's built-in hooks cover the majority of real-world state needs. Start here and only escalate when you have a concrete problem to solve.

  • Use useState for simple, independent values (a toggle, an input value, a counter).
  • Use useReducer when several state fields update together, when the next state depends on the previous state in complex ways, or when you want all transitions described in one place. It also pairs well with Context for app-scoped state that doesn't change often.
When to use React state

Keep state local when its consumers and update logic remain within one cohesive part of the tree.

  • The state is only relevant to one component or a small subtree
  • The state can be lifted to the closest common ancestor without prop-drilling getting painful
  • You don't need cross-tree access, persistence, or devtools
Example

This counter needs no sharing mechanism because the state belongs to one component:

import { useState, useReducer } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
}
function reducer(state, action) {
switch (action.type) {
case 'add':
return { items: [...state.items, action.item] };
case 'remove':
return { items: state.items.filter((i) => i.id !== action.id) };
default:
return state;
}
}
function Cart() {
const [state, dispatch] = useReducer(reducer, {
items: [{ id: 'book', name: 'Book' }],
});
return (
<ul>
{state.items.map((item) => (
<li key={item.id}>
{item.name}{' '}
<button onClick={() => dispatch({ type: 'remove', id: item.id })}>
Remove
</button>
</li>
))}
</ul>
);
}

React context

Context is a value-transport mechanism rather than a complete state-management solution. It lets descendants read a value without prop-drilling, but it does not supply storage or update logic. Consumers subscribed to a context re-render when the closest provider receives a different value according to Object.is; components that do not read that context are not updated for that reason. Putting unrelated, frequently changing values into one context can therefore cause unnecessary consumer renders.

Context works well for values such as theme, locale, current user, and feature flags. Pairing it with useState or useReducer can form a perfectly reasonable app-level state solution. If updates are frequent and different consumers need independent slices, split contexts or consider an external store with selectors.

In React 19, you can call Provider directly on the context (<ThemeContext> instead of <ThemeContext.Provider>) and read a context conditionally with the new use(Context) API:

import { createContext, use, useMemo, useState } from 'react';
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
// React 19: <Context> works as a Provider directly
return <ThemeContext value={value}>{children}</ThemeContext>;
}
function ThemedButton({ enabled }) {
if (!enabled) return null;
// `use` can be called conditionally; `useContext` cannot
const { theme, setTheme } = use(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
);
}
When to use Context

Context fits values that many descendants need and that can tolerate its broadcast-style subscription behavior.

  • Passing rarely-changing values (theme, locale, current user, router) deep into the tree
  • Avoiding prop-drilling for a value the whole subtree needs
  • Combined with useReducer for app-scoped settings
When not to use Context

Avoid one broad context when updates are frequent or consumers need independent subscriptions.

  • High-frequency updates (form input on every keystroke, animation state)
  • Independent slices that should not re-render each other — split into multiple contexts or use a store with selectors

External client-state libraries

When many unrelated components need to read and write the same frequently-changing client state, an external store can notify only subscribers whose selected slice changed. External stores can also provide middleware, devtools, and persistence.

  • Zustand — a small hook-based store with selector subscriptions and no required provider.
  • Jotai — an atomic, bottom-up model where each piece of state is an atom; an atom update notifies consumers that read the changed atom.
  • Redux Toolkit (RTK) — the modern, batteries-included Redux. The legacy createStore from redux is deprecated; use configureStore + createSlice. RTK includes Redux DevTools (which support time-travel debugging) and pairs with RTK Query for server state.
  • MobX — observable/reactive model, popular in some enterprise codebases.
  • Recoil is no longer a current choice because its repository was archived in 2025.
When to use an external store

An external store becomes useful when subscription control or capabilities outside the component tree outweigh the added dependency.

  • Many unrelated components share the same updates and Context causes too many re-renders
  • You need devtools, middleware (logging, persistence, undo/redo), or strict action-based update flows
  • State needs to live outside the React tree (e.g. accessed from a non-React module)
Example with Redux Toolkit

Redux Toolkit centralizes named transitions and exposes selector-based reads through React Redux:

// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: (state) => {
state.count += 1; // RTK uses Immer, so direct mutation is fine
},
},
});
export const { increment } = counterSlice.actions;
export default counterSlice.reducer;
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: { counter: counterReducer },
});
// Counter.js
import { useSelector, useDispatch } from 'react-redux';
import { increment } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();
return <button onClick={() => dispatch(increment())}>Count: {count}</button>;
}
// App.js
import { Provider } from 'react-redux';
import { store } from './store';
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
Example with Zustand

Zustand exposes a store Hook whose selector determines the slice a component subscribes to:

import { create } from 'zustand';
const useCounter = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}));
function Counter() {
const count = useCounter((s) => s.count);
const increment = useCounter((s) => s.increment);
return <button onClick={increment}>Count: {count}</button>;
}

Server state vs client state

A critical modern decision axis: data fetched from a server is not ordinary client state. It has a cache, can go stale, needs revalidation, refetching on focus, deduplication, retries, and pagination. Hand-rolling all of this on top of useState or Redux is error-prone.

For server data that needs a client cache, consider a dedicated server-state library:

  • TanStack Query (formerly React Query) — a widely used, framework-agnostic option.
  • SWR — lightweight alternative from Vercel.
  • RTK Query — built into Redux Toolkit, integrates with the same store.

Keeping cached server data separate from client-only state can simplify the client store and make invalidation rules explicit.

import { useQuery } from '@tanstack/react-query';
function Profile({ id }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', id],
queryFn: async () => {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
});
if (isLoading) return <Spinner />;
if (error) return <Error error={error} />;
return <h1>{data.name}</h1>;
}

Quick decision guide

The following table summarizes common starting points rather than hard requirements:

Choosing an owner for application state
SituationUse
State used by one componentuseState
Several related fields, complex transitionsuseReducer
Same value needed deep in the tree, changes rarelyContext (+ useReducer if needed)
Frequently-changing client state shared widelyZustand / Jotai / Redux Toolkit
Data fetched from a serverTanStack Query / SWR / RTK Query
Form stateReact Hook Form / TanStack Form
URL-driven state (filters, tabs)Router search params (e.g. TanStack Router, Next.js)

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

Which state-management choices fit their stated requirements? Select all that apply.

Explain the composition pattern in React

Topics
React

TL;DR

The composition pattern in React is the practice of building UIs by combining smaller, reusable components instead of extending them through inheritance. The most common forms are: passing children (props.children), passing components as named props (slots), specialization (a more specific component that wraps a generic one and fixes some props), render props / "children as a function", and compound components (a parent component that exposes a set of related sub-components, e.g. <Tabs> with <Tabs.List> and <Tabs.Panel>). Composition is React's main reuse mechanism, alongside custom hooks for behavior.


Composition pattern in React

Composition builds larger interfaces by arranging components through props and nesting instead of extending component classes.

What is composition?

Composition is a design principle that involves combining smaller, reusable components to build more complex components. In React, this is preferred over inheritance for creating complex UIs.

How to use composition in React

React supports several composition shapes depending on whether a component needs one content region, named regions, or component-specific configuration.

Passing components as children

One common way to use composition is by passing components as children to other components. This allows you to nest components and create a hierarchy.

function Dialog(props) {
return <div className="dialog">{props.children}</div>;
}
function WelcomeDialog() {
return (
<Dialog>
<h1>Welcome</h1>
<p>Thank you for visiting our spacecraft!</p>
</Dialog>
);
}
Passing components as props

Another way to achieve composition is by passing components as props. This allows for more flexibility and customization.

function SplitPane(props) {
return (
<div className="split-pane">
<div className="split-pane-left">{props.left}</div>
<div className="split-pane-right">{props.right}</div>
</div>
);
}
function App() {
return <SplitPane left={<Contacts />} right={<Chat />} />;
}
Specialization via props

A specialized component wraps a generic one and fixes some of its props. This is React's preferred alternative to subclassing.

function Dialog({ title, children }) {
return (
<div className="dialog">
<h2>{title}</h2>
{children}
</div>
);
}
function WelcomeDialog() {
return (
<Dialog title="Welcome">
<p>Thank you for visiting our spacecraft!</p>
</Dialog>
);
}

WelcomeDialog is a Dialog with a fixed title — no inheritance required.

Render props / children as a function

Sometimes the parent owns state but wants the consumer to decide how to render it. Passing a function as children (or as a named prop) gives the consumer full control:

function MouseTracker({ children }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
return (
<div onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}>
{children(pos)}
</div>
);
}
function App() {
return (
<MouseTracker>
{({ x, y }) => (
<p>
Mouse at {x}, {y}
</p>
)}
</MouseTracker>
);
}

In modern React, custom hooks usually replace the render-prop pattern for behavior reuse, but it remains useful when the shared piece is a piece of JSX layout, not just a value.

Compound components

Compound components let a parent expose a set of related sub-components that share implicit state via context. The consumer composes them in whatever structure they need:

<Tabs defaultValue="account">
<Tabs.List>
<Tabs.Trigger value="account">Account</Tabs.Trigger>
<Tabs.Trigger value="billing">Billing</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="account">Account settings</Tabs.Panel>
<Tabs.Panel value="billing">Billing details</Tabs.Panel>
</Tabs>

This is the API style used by libraries like Base UI, Ariakit, and React Aria, and by HTML itself (<select> with <option>).

Benefits of composition

Composition keeps responsibilities local while allowing callers to control how reusable pieces are assembled.

  • Reusability: Smaller components can be reused across different parts of the application.
  • Maintainability: Easier to manage and update smaller components.
  • Flexibility: Components can be combined in many ways to create complex UIs without each combination needing its own component.
  • No inheritance traps: Avoids the tight coupling of class hierarchies and the diamond problem.

Composition vs HOCs vs hooks

Before hooks, behavior reuse often went through higher-order components (HOCs like withRouter, connect) or render props. Both work but tend to introduce wrapper components that show up in the tree and make types and refs awkward to forward. Custom hooks (introduced in React 16.8) cover most of those use cases more cleanly. Today the rule of thumb is:

  • Reuse a piece of UI structure with composition (children, slots, compound components).
  • Reuse a piece of behavior or state with a custom hook.
  • Reach for HOCs only when wrapping is genuinely what you need (e.g. integrating with a library that expects them).

When to use composition

Composition is a good default when variation can be expressed by arranging existing UI pieces or passing explicit content regions.

  • When you need to build complex UIs from smaller, reusable components.
  • When you want a generic component (e.g. Dialog, Card) to support arbitrary content via children or slots.
  • When you want to avoid inheritance, which React explicitly recommends against for component reuse.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise 1 of 2
Check your understanding Exercise 1 of 2

A shared dialog owns its border, heading layout, and close button. One caller needs a form in the body and two action buttons; another needs a chart and a download link. Which API lets callers supply these differences while the dialog keeps the shared shell?

What is React? Describe the benefits of React