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 a JavaScript library created by Facebook for building user interfaces, primarily for single-page applications. It allows developers to create reusable components that manage their own state. Key benefits of React include a component-based architecture for modular code, the virtual DOM for efficient updates, a declarative UI for more readable code, one-way data binding for predictable data flow, and a strong community and ecosystem with abundant resources and tools.

Key characteristics of React:

  • Declarative: You describe the desired state of your UI based on data, and React handles updating the actual DOM efficiently.
  • Component-based: Build reusable and modular UI elements (components) that manage their own state and logic.
  • Virtual DOM: React uses a lightweight in-memory representation of the actual DOM, allowing it to perform updates selectively and efficiently.
  • JSX: While not mandatory, JSX provides a syntax extension that allows you to write HTML-like structures within your JavaScript code, making UI development more intuitive.

What is React?

React is an open-source JavaScript library developed by Meta (formerly Facebook) for building user interfaces. It focuses on the view layer of an application and is especially useful for creating single-page applications where a seamless user experience is crucial. React allows developers to build encapsulated components that manage their own state and compose them to create complex UIs.

Modern React is hooks-first and function-component-driven. JSX, an HTML-like syntax extension to JavaScript, is the standard way to describe UI. With React 19 (released December 2024), React also officially supports React Server Components, the use() hook for unwrapping promises and context, Actions for form/data mutations, and the React Compiler (RC), which automatically memoizes components so you rarely need to reach for useMemo/useCallback by hand.

Benefits of React

1. Component-based architecture

React encourages breaking down your UI into independent, reusable components. Each component encapsulates its own state, logic, and rendering, making your code:

  • Modular and reusable: Components can be easily reused across different parts of your application or even in other projects.
  • Maintainable: Changes within a component are isolated, reducing the risk of unintended side effects.
  • Easier to test: Components can be tested independently, ensuring their functionality and reliability.

2. Virtual DOM and efficient updates

React utilizes a virtual DOM, a lightweight in-memory representation of the actual DOM. When data changes, React first updates the virtual DOM, then compares it to the previous version. This process, known as diffing, allows React to identify the minimal set of changes required in the actual DOM. By updating only the necessary elements, React minimizes expensive DOM manipulations, resulting in significant performance improvements.

3. Large and active community

React boasts a vast and active community of developers worldwide. This translates to:

  • Extensive documentation and resources: Find comprehensive documentation, tutorials, and community-driven resources to aid your learning and development process.
  • Abundant third-party libraries and tools: Leverage a rich ecosystem of pre-built components, libraries, and tools that extend React's functionality and streamline development.
  • Strong community support: Seek help, share knowledge, and engage with fellow developers through forums, online communities, and meetups.

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 communicate back up through callbacks rather than mutating shared state directly. This makes the flow of data explicit and easy to trace, which leads to more predictable behavior and simpler debugging compared to two-way binding approaches.

5. Hooks and function components

Modern React is built around function components and hooks. Hooks like useState, useEffect, useReducer, useContext, and useRef let you manage state, side effects, and shared logic without classes. Custom hooks make it easy to extract and reuse stateful logic across components.

6. Learn once, write anywhere

React's versatility extends beyond web development. With React Native, you can apply your React knowledge to build native mobile applications for iOS and Android, sharing components and logic across web and mobile and targeting multiple platforms without learning entirely new technologies.

Further reading

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

Topics
React

TL;DR

A React Node is anything React can render: a React Element, a string, a number, an array of nodes, a fragment, a portal, null, undefined, false, or true. A React Element is the immutable plain object React produces from JSX or React.createElement describing what to render. A React Component is a function (or, historically, a class) that accepts props and returns React Nodes. Elements describe the UI; components are the factories that produce those elements.


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 and numbers (rendered as text)
  • Arrays of React Nodes (commonly produced by .map(...))
  • Fragments (<>...</> or <React.Fragment>)
  • Portals (created with createPortal)
  • 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!',
);

Internally each element carries a $$typeof symbol (Symbol.for('react.element') / 'react.transitional.element' in newer versions) that React and serializers use to recognise a real element and prevent injection attacks from JSON. Elements are cheap to create and are throwaway descriptions — React diffs them against the previous tree and updates the actual DOM.

React component

A React Component accepts inputs (props) and returns React Nodes that describe the UI. 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 still work but are legacy in React 19 — there are no new class-only features, the legacy lifecycles (componentWillMount, componentWillReceiveProps, componentWillUpdate) and string refs are gone, and new APIs (Hooks, Server Components, the React Compiler) target functions only. Prefer functions for new code.

    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."

Further reading

What is JSX and how does it work?

Topics
React

TL;DR

JSX stands for JavaScript XML. It is a syntax extension for JavaScript that allows you to write HTML-like code within JavaScript. JSX makes it easier to create React components by allowing you to write what looks like HTML directly in your JavaScript code. Under the hood, JSX is transformed into JavaScript function calls, typically using a tool like Babel. For example, <div>Hello, world!</div> in JSX is transformed into React.createElement('div', null, 'Hello, world!').


What is JSX and how does it work?

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.

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.
  • URL-valued attributes such as href and src are not sanitized by React — href="javascript:..." will execute if the URL string comes from an attacker-controlled source. Validate URLs yourself.

Further reading

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 is not allowed to mutate. State changes trigger a re-render of the owning component (and its descendants); prop changes happen because the parent re-rendered with new values. 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

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 are asynchronous and batched — React groups updates triggered in the same event (and, since React 18, also in promises, timeouts, and native event handlers) and re-renders once.
  • 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);
// Wrong: reads a possibly stale `count` if multiple updates batch together
// 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>
);
}

Class components use this.setState, but writing this.setState({ count: this.state.count + 1 }) is the exact stale-read anti-pattern the React docs warn against — pass a function (this.setState(prev => ({ count: prev.count + 1 }))) for the same reason. New code should use useState in a function component.

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

StateProps
Owned byThe component itselfThe parent
Mutable byThe component (via its setter)No one (read-only in the child)
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

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?

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

  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

  • 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

  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.
// Bad: array index as key in a list that can change order.
<ul>
{items.map((item, index) => (
<ListItem key={index} value={item.value} />
))}
</ul>
// Good: stable unique identifier from your data.
<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

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

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 box on the second row and then deletes the first row, the second item's data shifts up to index 0, but React keeps the original <li key={0}> mounted with its checked checkbox and previously typed text. The visible label changes; the DOM state does not. Switching to key={todo.id} fixes it because React then unmounts the deleted row and keeps the surviving row's DOM intact.

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

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?

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. Pairing value with no onChange (or vice versa) makes the input read-only or warns in development; pick one mode per field.

<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(prevState, formData) {
const name = formData.get('name');
// ...persist...
return { ok: true, name };
}
function NameForm() {
const [state, formAction] = useActionState(saveName, null);
return (
<form action={formAction}>
<input name="name" defaultValue={state?.name ?? ''} />
<SubmitButton />
</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

State management
  • Controlled: React state owns the value; the DOM mirrors it.
  • Uncontrolled: The DOM owns the value; React reads it on demand.
Data flow
  • Controlled: state -> value -> input, and onChange -> setState.
  • Uncontrolled: defaultValue -> input, then ref.current.value (or FormData) when read.
When to use which
  • 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

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/array as the provider value on every render (which forces every consumer to re-render), assuming React.memo will stop context-driven re-renders (it won't), and reaching for context as a general-purpose state manager. For frequently-changing or independent slices of state, split context into multiple providers, memoize the value, or use a dedicated state library like Redux, Zustand, or Jotai.


Pitfalls of using context in React

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 (or letting the React Compiler do it for you):

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 is a transport mechanism for passing values down the tree, not a state manager. It has no caching, no devtools, no middleware, no fine-grained updates. For complex client state, prefer Redux Toolkit, Zustand, or Jotai. For server state (data from APIs), use TanStack Query, SWR, or RTK Query — don't store fetched data in context.

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) instead of useContext

In React 19 you can read a context with the new use API. Unlike useContext, use can be called inside conditionals and loops:

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

What are the benefits of using hooks in React?

Topics
React

TL;DR

Hooks let you use state and other React features in plain functions, without classes. They were introduced to solve real pain points in the class-component era — "wrapper hell" from HOCs and render props, the awkwardness of this binding, and the difficulty of sharing stateful logic between components. Custom hooks make that logic genuinely reusable through composition. React 19 expands the set further with hooks like use, useActionState, useOptimistic, and useFormStatus for promises, forms, and optimistic UI.


Benefits of using hooks in React

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. Both produced deeply nested component trees — the so-called "wrapper hell" you'd see in React DevTools — and made it hard to follow data flow. Class components also forced you to bind this for handlers, split related logic across componentDidMount/componentDidUpdate/componentWillUnmount, and made it nearly impossible to extract a piece of stateful behaviour without restructuring the component tree.

Hooks solve all of these by moving stateful logic into plain functions you can compose, with no this, no extra wrappers, and no lifecycle-method scatter.

Reusable logic via custom hooks

Custom hooks let you extract any combination of state, effects, and other hooks into a function that any component can call. This is composition rather than inheritance, and it scales much better than HOCs — you can use as many custom hooks in a component as you want without nesting.

function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const on = () => setIsOnline(true);
const off = () => setIsOnline(false);
window.addEventListener('online', on);
window.addEventListener('offline', off);
return () => {
window.removeEventListener('online', on);
window.removeEventListener('offline', off);
};
}, []);
return isOnline;
}
function StatusBar() {
const isOnline = useOnlineStatus();
return <div>{isOnline ? 'Online' : 'Offline'}</div>;
}

Because custom hooks are just function calls, you also break complex components into smaller, named pieces of logic — making them easier to read.

Simplified state management

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

const [count, setCount] = useState(0);

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:

useEffect(() => {
// setup (mount + when dependencies change)
return () => {
// cleanup (unmount + before next run)
};
}, [dependencies]);

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 unlock new patterns

React 19 adds several hooks 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.

Combined with the React Compiler (RC/stable by 2026), these reduce a lot of the manual useMemo/useCallback and form-state boilerplate that earlier hook-based code needed.

Further reading

What are the rules of React hooks?

Topics
React

TL;DR

React hooks have a few essential rules to ensure they work correctly. Always call hooks at the top level of your component or custom hook — never inside loops, conditions, nested functions, or after an early return. Only call hooks from React function components or other custom hooks (whose names must start with use). Lean on eslint-plugin-react-hooks to enforce these rules. The React Compiler (RC/stable by 2026) relaxes the need for some manual memoization, but the rules of hooks themselves still apply.


What are the rules of React hooks?

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.

// Correct
function MyComponent({ enabled }) {
const [count, setCount] = useState(0);
if (!enabled) {
// Use the value conditionally — fine.
}
return <div>{count}</div>;
}
// Incorrect — hook inside an `if`
function MyComponent({ 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 MyComponent({ 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:

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

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.

// 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's how the linter identifies a custom hook and enforces the rules of hooks inside it. Naming a function getCounter instead of useCounter will silently disable those checks.

Use eslint-plugin-react-hooks

The eslint-plugin-react-hooks package automates enforcement of these rules. It ships two main rules: react-hooks/rules-of-hooks (call order, where hooks may be called) and react-hooks/exhaustive-deps (correct dependency arrays for useEffect, useMemo, useCallback).

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

For an ESLint legacy .eslintrc config:

{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}

For ESLint 9's flat config (eslint.config.js), import the plugin and spread its recommended config:

import reactHooks from 'eslint-plugin-react-hooks';
export default [
{
plugins: { 'react-hooks': reactHooks },
rules: reactHooks.configs.recommended.rules,
},
];

eslint-plugin-react-hooks v5 added flat-config support and ships an additional rule set for the React Compiler when you enable it.

A note on the React Compiler

The React Compiler (RC and on track for stable in 2026) auto-memoizes components and values, removing most of the need for hand-written useMemo and useCallback. It does not change the rules of hooks — your hooks still have to be called unconditionally at the top level, from components or custom hooks. The compiler actually relies on those rules to do its job safely, and will refuse to optimize components that violate them.

Further reading

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 asynchronously after the browser has painted. It does not block the user from seeing the new frame. Use it for data fetching, subscriptions, logging, and most side effects.
  • 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, both fire twice on mount in Strict Mode development builds, and useLayoutEffect has no effect during server rendering (React warns if you use it in SSR).

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 a side effect to run after React commits changes to the DOM and the browser has painted. It is non-blocking — the user sees the new frame first, then the effect runs.

  • It is the right default for data fetching, subscriptions, event listeners, and logging.
  • The dependency array controls when it re-fires: [a, b] means "after any render where a or b changed (by Object.is)"; [] means "only after mount"; omitting the array means "after every render."
  • In Strict Mode during development, React mounts, unmounts, and remounts each component once, so effects (and their cleanup) fire twice. This surfaces missing cleanup logic. Production runs each effect once.

Code example

import { useEffect } from 'react';
function Example() {
useEffect(() => {
console.log('Mounted');
return () => console.log('Cleanup on unmount');
}, []); // [] deps: cleanup runs only on unmount
return <div>Hello, World!</div>;
}

With [] deps, the cleanup runs only when the component unmounts. With non-empty deps like [userId], the cleanup runs before the next effect fires (when userId changes) and again on unmount.

Common use cases

  • 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.
  • During server rendering it does nothing (there is no layout to measure) and React logs a warning if you schedule one. Either gate it behind a client check or reach for useEffect instead. For libraries that need to pick one based on environment, useInsertionEffect (see below) or a useIsomorphicLayoutEffect pattern is common.

Code example

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

  • 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 fires even earlier than useLayoutEffect — before React makes any DOM mutations. It exists for CSS-in-JS libraries that need to inject <style> tags before layout effects read them. 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

Timing

  • useEffect: Fires asynchronously after the browser paints.
  • useLayoutEffect: Fires synchronously during commit, before the browser paints.

Blocking behavior

  • useEffect: Non-blocking. Users see the new frame immediately.
  • useLayoutEffect: Blocks paint. The browser cannot repaint until the effect (and any resulting state update) has settled.

SSR behavior

  • useEffect: Skipped on the server (client runs it after hydration).
  • useLayoutEffect: No-ops on the server and warns; avoid it in isomorphic code or gate it on typeof window !== 'undefined'.

Use case examples

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

Further reading

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.

// Modern hooks form (preferred)
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((c) => c + 1);
setCount((c) => c + 1); // Both run; final count is +2.
};
// Legacy class form
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment,
}));

Purpose of the updater function form of setState

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"). Recognising that name is helpful in interviews.

Why it exists: state updates are batched and asynchronous

State setters do not change state immediately. React queues the update and applies it later, then re-renders. Since React 18, this batching is automatic and applies everywhere — event handlers, promises, setTimeout, native event handlers — not only inside React event handlers as in earlier versions.

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:

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

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

  • 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 — by then the closed-over value is almost certainly stale.
  • Inside useEffect or useCallback where omitting the state from deps would otherwise cause stale-closure bugs — using the updater lets you drop the value from the dependency array.

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:

class Counter extends React.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

What does the dependency array of `useEffect` affect?

Topics
React

TL;DR

The dependency array of useEffect determines when the effect should re-run. If the array is empty, the effect runs only once after the initial render. If it contains variables, the effect runs whenever any of those variables change. If omitted, the effect runs after every render.


What does the dependency array of useEffect affect?

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

  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: in development with Strict Mode, React intentionally mounts, unmounts, and remounts each component to surface bugs in effect cleanup. Your effect (and its cleanup) will run twice on the initial mount in development. Production runs the effect once.
    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
    });

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

  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

    In React 19, useEffectEvent (now stable) provides a way to read the latest value of a prop or state from inside an effect without making the effect re-subscribe on every change. It is the recommended escape hatch for the "I want the latest value, but I do not want this effect to re-run" pattern:

    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
  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

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 React, { useRef, useEffect } 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?

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:

  • Do not read or write ref.current during rendering. React only guarantees the ref's value is settled after commit; mutating it during render makes components impure and is disallowed.
  • It is fine (and expected) to read or write ref.current inside event handlers, effects, or callbacks.

Key use cases for useRef

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

In this example, the useRef hook is used to create a reference to the input element, and the useEffect hook is used to focus the input element when the component mounts. The optional chaining (?.) guards against the rare case where the element is not yet attached, which is a good habit under React 19's stricter dev-mode checks.

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 React, { useRef, useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(undefined);
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<div>
<h1>Now: {count}</h1>
<h2>Before: {prevCountRef.current}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

Here prevCountRef holds the previous value of count across renders, but updating it does not itself cause a re-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 deprecated

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 deprecated and scheduled for removal 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.

<input
ref={(node) => {
node.focus();
return () => {
// runs when the element unmounts or the ref changes
};
}}
/>

Further reading

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

Topics
React

TL;DR

useCallback returns a memoized function whose identity only changes when one of its dependencies changes. The point is referential stability — so a React.memo-wrapped child does not re-render, or a useEffect whose deps include the function does not re-fire. Re-creating a plain function literal each render is essentially free; the actual cost being avoided is the downstream work triggered by a new reference.

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

Note for 2026: with the React Compiler (stable in React 19), most components no longer need manual useCallback / useMemo / React.memo — the compiler memoizes automatically. New code targeting a compiler-enabled project should generally not reach for useCallback unless profiling shows a specific need.


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

What is useCallback?

The useCallback hook is a React hook that returns a memoized version of the callback function that only changes if one of the dependencies has changed. It is useful for optimizing performance by preventing unnecessary re-creations of functions.

Syntax

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

When should useCallback be used?

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.

A common bug is depending on a state value you also update inside the callback — the dependency changes after every click, so the memoization is useless:

// BAD: `count` is in the deps, so `handleClick` gets a new identity every click,
// defeating React.memo on the child.
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);

The fix is to use the functional updater form of setState, which lets you drop the value from the deps:

const ParentComponent = () => {
const [count, setCount] = useState(0);
// GOOD: empty deps, stable identity for the lifetime of the component.
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);
return <ChildComponent onClick={handleClick} />;
};
const ChildComponent = React.memo(({ onClick }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
});
Stabilising 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 fetchData = useCallback(async () => {
const res = await fetch(`/api/items?query=${query}`);
setItems(await res.json());
}, [query]);
useEffect(() => {
fetchData();
}, [fetchData]);
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

  • It is not free: useCallback itself adds bookkeeping. For a function that is not passed to a memoized child or used in a dependency array, wrapping it usually makes the code slower, not faster.
  • The cost being avoided is downstream re-renders, not the function literal itself. Allocating an arrow function on each render is essentially free.
  • Dependencies must be correct: missing dependencies cause stale closures; over-specified ones defeat the memoization.
  • The React Compiler removes most need for it: in a compiler-enabled codebase, prefer plain inline functions and let the compiler memoize.

Further reading

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

Topics
React

TL;DR

The useMemo hook in React is used to memoize expensive calculations so that they are only recomputed when one of the dependencies has changed. This can improve performance by avoiding unnecessary recalculations. You should use useMemo when you have a computationally expensive function that doesn't need to run on every render.

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

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

What is useMemo?

The useMemo hook is a built-in React hook that allows you to memoize the result of a function. This means that the function will only be re-executed when one of its dependencies changes. The primary purpose of useMemo is to optimize performance by preventing unnecessary recalculations.

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. The memoized value will only be recomputed when one of these dependencies changes.

When should it be used?

Expensive calculations

If you have a function that performs a computationally expensive calculation, you can use useMemo to ensure that this calculation is only performed when necessary. Genuinely expensive work means things like filtering or sorting a large list, parsing a big payload, or running a heavy synchronous algorithm — not trivial arithmetic.

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.

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].sort((a, b) => a - b), [items]);
return <MemoChild sortedItems={sortedItems} />;
};
const MemoChild = React.memo(function MemoChild({ sortedItems }) {
return <ul>{/* ... */}</ul>;
});

Note that useMemo on a value alone does not prevent the child from re-rendering — the child must also be wrapped in React.memo (or otherwise short-circuit). Also, [...items].sort(...) (or items.toSorted(...)) is used here because Array.prototype.sort mutates in place; mutating a prop is a bug waiting to happen.

Caveats

  • 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 hint: useMemo is a performance hint, not a guarantee. React is allowed to throw away the cached value and recompute it (for example, to free memory). Never rely on useMemo for correctness — only for optimization.
  • The React Compiler changes the calculus: As of 2026, the React Compiler is stable. When enabled, it auto-memoizes components and values for you, which largely removes the need to write useMemo (and useCallback) by hand. New codebases on the Compiler should reach for useMemo only for genuinely expensive work that the compiler cannot infer, or to satisfy a specific reference-equality requirement.

Further reading

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?

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);
}
}

Example usage

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

import React, { 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. When provided, React calls init(initialArg) once on mount and uses the result as the initial state. This is useful when computing the initial state is expensive, or when you want to derive it from a prop.

function init(initialCount) {
return { count: initialCount };
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
// ...
}

Bailing out of an update

If the reducer returns the exact same value (compared with Object.is) as the current state, React bails out — no re-render is scheduled and no children re-render. This is one reason reducers must be pure and must not mutate the existing state object: a mutated-but-same-reference return looks like a bailout to React, but the state has actually changed.

When to use useReducer

  • 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

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?

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++;) caused hydration mismatches: the server and the client could increment the counter in a different order, producing different IDs in the HTML versus the client tree. React would then warn (and in React 18+, throw away the hydrated subtree).

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 counterNo (server and client increment in different orders)Yes (within a session)Was the pre-React 18 workaround. Replaced by useId.

If an ID appears in rendered output, it must come from useId. If an ID belongs to your data (a row's primary key, a user's session ID, and so on), generate it outside render and store it.

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 a widget you embed into a host page that also uses React), two separate roots can both generate :r0: and collide.

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

Important distinction: useId is not a replacement for list keys. key identifies a piece of data across renders so React can reconcile additions, removals, and reorders. useId identifies a component position in the tree, so it stays the same when the list is reordered, which is the wrong behavior for a key. Always use a stable ID from your data (a database id, a slug, and so on) for list keys.

  • 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

  • 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

What does re-rendering mean in React?

Topics
React

TL;DR

Re-rendering in React refers to the process where a component updates its output to the DOM in response to changes in state or props. When a component's state or props change, React triggers a re-render to ensure the UI reflects the latest data. This process involves calling the component's render method again to produce a new virtual DOM, which is then compared to the previous virtual DOM to determine the minimal set of changes needed to update the actual DOM.


What does re-rendering mean in React?

Understanding re-rendering

Re-rendering is the process by which React calls a component's function again to compute a new description of the UI. It is important to note that a re-render does not necessarily result in a DOM update — React performs reconciliation against the previous output and may bail out if nothing has changed.

When does re-rendering occur?

Re-rendering occurs in the following scenarios:

  • When a component's state changes via a useState setter or a useReducer dispatch
  • When a component subscribed to a context reads a new context value
  • When a component receives new props from its parent
  • When a parent component re-renders, its children re-render by default — even if their props are referentially unchanged
  • 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

  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 minimal set of changes (if any) to the actual DOM.

Example

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

import React, { 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

Re-rendering itself is usually cheap, but unnecessary re-renders of expensive subtrees can hurt performance. React provides several techniques to opt out of re-rendering:

  • React.memo: A higher-order component that memoizes a function component's output and skips re-rendering if its props are referentially equal (compared with Object.is).
  • 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

The React Compiler (RC/stable as of 2025) automatically memoizes components, values, and callbacks at build time. With it enabled, much of the manual useMemo / useCallback / React.memo work becomes unnecessary — the compiler inserts the optimal memoization for you. The mental model still matters for debugging, but the optimization story is shifting from "manually memoize hot paths" to "write idiomatic code and let the compiler memoize."

Further reading

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?

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.

Common misconception: Fragments are often described as a performance optimization, on the assumption that one extra <div> per component is a meaningful cost. In practice, the cost is negligible. Browsers can render thousands of additional DOM nodes without measurable impact. The actual reasons to reach for a fragment are structural, not performance-related.

  • 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. key is the only prop React.Fragment accepts.

    return (
    <React.Fragment>
    <ChildComponent1 />
    <ChildComponent2 />
    </React.Fragment>
    );

When to use <Fragment> instead of <>

The shorthand <>...</> is concise but does not accept any props, including key. The single case that requires the explicit React.Fragment form is when a key is needed, which happens whenever a map produces items that each render more than one sibling element.

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>
))}

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.

Use cases

  • 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

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

Topics
React

TL;DR

As of React 19 (December 2024), forwardRef() is deprecated. Function components can now accept ref as a regular prop, so wrapping in forwardRef() is no longer required. 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?

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>
);
}

The React team ships a codemod that automatically converts existing forwardRef() usages to the new prop form. forwardRef() itself still works for now but logs a deprecation warning and is expected to be removed in a future major.

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 deprecated
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

  • 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

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?

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.

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). The initializer runs only on the first render, so re-renders don't recompute it.

import { useState } from 'react';
function MyComponent({ initialCount }) {
// The function runs once, on mount. 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

Class components are no longer the recommended way to write React code in 2026 — function components with hooks (and the key reset pattern above) cover every case. If you're maintaining legacy code, you can reset class state by calling this.setState(this.initialState), but new code should use hooks.

Further reading

Why does React recommend against mutating state?

Topics
React

TL;DR

React recommends against mutating state because several of its mechanisms depend on the previous and next state being different objects (reference inequality). When you mutate state in place, the reference does not change, which breaks Object.is bailouts in useState/useReducer, breaks React.memo and useMemo/useEffect dependency comparisons, and can cause tearing under concurrent rendering. It also defeats time-travel debugging in React DevTools. Always produce a new object/array (with the spread operator, array methods like map/filter/toSorted, or a library such as Immer) and pass it to the state setter.


Why does React recommend against mutating state?

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.
  • Concurrent rendering and tearing: Since React 18, rendering can be interrupted, paused, or even abandoned. If you mutate state during a render, an in-progress render and a discarded render can read different values from the same object — producing tearing, where different parts of the UI reflect different versions of the state.
  • DevTools and time-travel debugging: React DevTools (and tools like Redux DevTools) rely on snapshots of past states to let you step backwards. Mutation overwrites those snapshots, so the history becomes meaningless.

React schedules a render either way

Calling setState/dispatch always schedules a render — React does not refuse to render because you passed the same reference. What the reference comparison controls is what happens after the render is queued: whether the component bails out (skips actually rendering), and whether downstream React.memo/useMemo/useEffect consumers see a "change." Mutation is a problem precisely because it produces "looks the same to React, actually different in memory" — the worst of both worlds.

Problems with mutating state

  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. Tearing under concurrent features such as startTransition and Suspense.
  4. Lost debuggability: Time-travel and "previous props/state" inspection in React DevTools stop working correctly.
  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

What are error boundaries in React for?

Topics
React

TL;DR

Error boundaries in React are components that catch JavaScript errors thrown during rendering, in lifecycle methods, and in constructors of their child component tree, then display a fallback UI instead of crashing the whole application. They are implemented as class components using static getDerivedStateFromError (to render a fallback) and optionally componentDidCatch (for logging). Since React 16, an uncaught error unmounts the entire React tree, which makes error boundaries effectively required for production apps. Error boundaries do not catch errors inside event handlers, asynchronous code, or server-side rendering. As of React 19, there is still no hooks-based API for error boundaries — most teams use the react-error-boundary library, or rely on the new root-level onUncaughtError, onCaughtError, and onRecoverableError options on createRoot/hydrateRoot.


What are error boundaries in React for?

Introduction

Error boundaries are a feature in React that help manage and handle errors in a more graceful way. They allow developers to catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of crashing the entire application.

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. This behavior makes wrapping at least the top of your app in an error boundary effectively mandatory for production.

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>

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 options on createRoot and hydrateRoot to handle errors that bubble all the way to the root, useful for global logging and analytics:

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

Because error boundaries must be class components and the API is fairly verbose, most teams use the react-error-boundary library. It exposes an <ErrorBoundary> component plus a useErrorBoundary hook for imperatively triggering a boundary from function components, which covers common cases like rethrowing errors caught in event handlers or async code.

Best practices

  • Use error boundaries to wrap high-level components such as route handlers or major sections of your application.
  • Log errors to an error reporting service to keep track of issues in production.
  • Provide a user-friendly fallback UI to improve the user experience when an error occurs.

Further reading

How do you test React applications?

Topics
React

TL;DR

To test React applications, use Jest or Vitest as the test runner together with React Testing Library, which encourages testing components the way users interact with them. Drive interactions with @testing-library/user-event (preferred over fireEvent), mock network calls with MSW, and write end-to-end tests with Playwright (or Cypress). For React 19 features like async components and Server Components, lean on async queries (findBy*, waitFor).


How do you test React applications?

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 work in unit tests, Storybook, and the dev server. It is the de facto standard 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 async and Server Components in React 19

React 19 introduced async components and stabilized Server Components. For client-rendered async components, await the data with findBy*/waitFor — RTL handles the suspended render automatically. For Server Components, run the component as a normal async function in a Node test and assert on the returned tree, or test them through an integration test with a framework like Next.js. Keep network calls mocked at the boundary (MSW, or by stubbing the data-fetching 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 has become the most popular choice for new projects — it ships parallel execution, multi-browser support, auto-waiting, and a strong trace viewer out of the box. Cypress is still widely used and a fine option in existing codebases.

// 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

Explain what React hydration is

Topics
React

TL;DR

React hydration is the process of attaching event listeners and making a server-rendered HTML page interactive on the client side. When a React application is server-side rendered, the HTML is sent to the client, and React takes over to make it dynamic by attaching event handlers and initializing state. This process is called hydration.


What is React hydration?

Server-side rendering (SSR)

Server-side rendering (SSR) is a technique where the HTML of a web page is generated on the server and sent to the client. This allows for faster initial page loads and better SEO since the content is already available when the page is loaded.

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. Reusing the existing HTML: React uses the HTML generated by the server and does not re-render it from scratch.
  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.

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 by attaching the onClick event listener, making it interactive. (Note: ReactDOM.hydrate from react-dom was removed in React 18 — always 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 a value is genuinely expected to differ between server and client (e.g. a timestamp or a value derived from window), add suppressHydrationWarning to silence the warning for that single element. Use it sparingly — it does not fix the mismatch, it only hides the warning.

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

Benefits of hydration

  1. Faster initial load: Since the HTML is already available, the initial page load is faster.
  2. SEO benefits: Search engines can crawl the server-rendered HTML, improving SEO.
  3. Improved user experience: Users can see the content immediately, even before React has fully taken over.

Challenges of hydration

  1. Mismatch issues: If the server-rendered HTML does not match the client-side React components, React logs an error. React 19 improved hydration error messages by showing a precise diff of the mismatching attributes/text instead of the previous vague "text content did not match" warning, making them much easier to debug.
  2. 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

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?

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

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';
const Modal = ({ isOpen, children }) => {
if (!isOpen) return null;
return createPortal(
<div className="modal">{children}</div>,
document.getElementById('modal-root'),
);
};
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(() => {
if (!targetRef.current) return;
const rect = targetRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
});
}, [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. The default ReactDOM namespace import was removed in React 19, so use the named import. createPortal takes two arguments: the child element 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.

Server-side rendering

createPortal requires a real DOM node, which doesn't exist during server-side rendering. The typical pattern is to render null on the server and only render the portal once mounted on the client — for example, by gating on a mounted state set in a useEffect, or by checking typeof window !== 'undefined'.

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 libraries like Radix UI, React Aria, or Headless UI rather than rolling their own.

Benefits

  • 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.
  • Simplified styling: By rendering components outside the parent component, you can avoid complex CSS rules and ensure the component is styled correctly.

Further reading

How do you debug React applications?

Topics
React

TL;DR

To debug React applications, use the React Developer Tools browser extension to inspect the component tree, props/state, and rendering with the Profiler. Enable Strict Mode during development to surface unsafe patterns, and rely on React 19.1 owner stacks for clearer component-aware stack traces. Use error boundaries (or the react-error-boundary package) to catch render-time errors, and reach for console.log, breakpoints, and the React DevTools "log" button for deeper inspection.


How do you debug React applications?

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.

Install from the Chrome Web Store or Firefox Add-ons.

Owner stacks (React 19.1)

React 19.1 introduced owner stacks, which surface the chain of components that rendered the failing component (the "owner" tree, what the developer wrote in JSX) instead of the parent fiber chain. Errors and warnings in the console now point to the JSX site that actually rendered the broken component, which is the single biggest day-to-day debugging improvement in years. Available in development builds via captureOwnerStack() and shown automatically in error overlays in modern frameworks.

Strict Mode

Wrap your tree in <StrictMode> during development to surface common bugs early. It intentionally double-invokes component bodies, useState initializers, reducers, and effects (mount → unmount → remount) so you notice impure renders, missing effect cleanup, and stale closures before they ship.

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 ("Why did this render?") over hand-rolled logs. The historical why-did-you-render library is mostly class-era and is rarely needed today, especially once the React Compiler removes most needless re-renders automatically.

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
class ErrorBoundary extends React.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 react-error-boundary Package

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

import { ErrorBoundary } from 'react-error-boundary';
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() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset the state of your app
}}
onError={(error, info) => {
// Log the error to an error reporting service
}}>
<MyComponent />
</ErrorBoundary>
);
}

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

import { useErrorBoundary } from 'react-error-boundary';
function MyComponent() {
const { showBoundary } = useErrorBoundary();
const handleAsyncError = async () => {
try {
await someAsyncOperation();
} catch (error) {
showBoundary(error);
}
};
return <button onClick={handleAsyncError}>Perform Action</button>;
}

Further reading

What is React strict mode and what are its benefits?

Topics
React

TL;DR

React strict mode is a development tool that helps identify potential problems in an application. It activates additional checks and warnings for its descendants. It doesn't render any visible UI and doesn't affect the production build. The benefits include identifying unsafe lifecycle methods, warning about legacy string ref API usage, detecting unexpected side effects, and ensuring that components are resilient to future changes.


What is React strict mode and what are its benefits?

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>,
);

If you're using a framework like Next.js, StrictMode is enabled in development by default, so you usually don't need to wire it up by hand.

Benefits of React strict mode

Detecting unexpected side effects (the headline behavior)

In React 18+, StrictMode intentionally double-invokes the following in development only:

  • Function component bodies (and class render/constructor)
  • useState, useMemo, and useReducer initializers and updater functions
  • Effects: each effect mounts, immediately unmounts, then mounts again (mount → unmount → remount)

The effect remount behavior is the most asked-about StrictMode behavior in interviews. It surfaces effects that aren't safely re-runnable — for example, an effect that subscribes to something but doesn't return a cleanup function will leak a subscription on every remount. Fixing your effects so the remount cycle is a no-op makes your components compatible with future features like reusable state across unmounts.

React 19 keeps this strict effect behavior and continues to use it as the contract function components are expected to satisfy.

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

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

Choosing a localization library

There are several libraries available for localizing React applications, with react-i18next and react-intl being among the most popular. For this guide, we'll focus on 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 React from 'react';
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 React from 'react';
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' },
});

Pair this with React Suspense so components render their fallback while a translation file loads:

i18n.init({ react: { useSuspense: true } });
<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 €"
new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long' }).format(new 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

  • 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 call t directly once i18n is initialized; pass the t function or translated strings down to Client Components rather than re-initializing i18n on the client when possible.

Further reading

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?

Introduction

Code splitting is a performance optimization technique that involves breaking down your application's code into smaller, more manageable chunks. This allows the application to load only the necessary code initially and defer the loading of other parts until they are needed. This can significantly reduce the initial load time and improve the overall user experience.

How to implement code splitting

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.

// Dynamic import example
import('./module').then((module) => {
// Use the module
});
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

Modern React frameworks handle code splitting for you. Next.js (App Router) and Remix / React Router (framework mode) automatically split per route and per Server / Client Component boundary, so you usually do not need lazy for routes — only for genuinely opt-in subtrees like modals, editors, or charts.

Pairing Suspense with error boundaries

Suspense only handles the loading state. If the dynamic import fails (network error, deploy mismatch), you need an error boundary to render a fallback and let the user retry:

<ErrorBoundary fallback={<RetryUI />}>
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
Suspense for data and use()

In React 19, Suspense integrates with data fetching through the use() hook — components can use(promise) to suspend on data the same way lazy suspends on a code chunk. This means a single <Suspense> boundary can coordinate both code loading and data loading.

Preloading

You can warm a lazy chunk before it is needed (e.g. on hover or focus) by simply calling the dynamic import — the bundler / browser will fetch and cache it:

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

Benefits of code splitting

  • Improved performance: By loading only the necessary code initially, you can reduce the initial load time of your application.
  • Better user experience: Faster load times lead to a smoother and more responsive user experience.
  • Efficient resource usage: Code splitting ensures that resources are used more efficiently by loading code only when it is needed.

Tools and libraries

  • Webpack / Rspack / Vite / Turbopack: Modern bundlers all support code splitting out of the box and split on dynamic import() boundaries automatically.
  • React Loadable: A predecessor to React.lazy that is now abandoned (unmaintained for years). New code should use lazy and Suspense instead.

Further reading

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

Topics
ReactPerformance

TL;DR

The first thing to know in 2026 is that the React Compiler auto-memoizes components and values, so a lot of the manual useMemo/useCallback work that used to be required for context performance is now done for you — adopt it before reaching for other tricks. Beyond that, the canonical patterns are: split a single context into a state context and a dispatch (or setter) context so consumers that only dispatch don't rerender on state changes; memoize the value object you pass to the provider; wrap consumer components in React.memo; and reach for selector libraries like use-context-selector when you need to subscribe to a slice of a large value.

const value = useMemo(() => ({ state }), [state]); // dispatch is already stable

How to optimize the performance of React contexts to reduce rerenders

Reach for the React Compiler first

The React Compiler (stable as of 2025) automatically memoizes component output, hook return values, and JSX based on dependency analysis of your code. For most applications this dramatically reduces the rerender pressure that motivates manual context optimization in the first place. Enable it in your build (babel-plugin-react-compiler for Babel/Vite/Next.js) and re-measure before adding hand-rolled memoization — many of the patterns below become unnecessary or shrink to a single annotation.

The compiler doesn't, however, change the fundamental rule of context: every consumer of a context rerenders whenever the provider's value reference changes. The patterns below are about controlling when that reference changes and which components see it.

Split state and dispatch into two contexts

The single most 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 don't rerender when state changes, because they only subscribe to the dispatch context whose value never changes.

import {
createContext,
useContext,
useReducer,
useMemo,
type ReactNode,
} from 'react';
const StateContext = createContext(null);
const DispatchContext = createContext(null);
function reducer(state, action) {
// ...
}
const initialState = { count: 0 };
export function CounterProvider({ children }: { children: ReactNode }) {
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() and never rerender when state updates. This is the pattern recommended in the React docs and used by most production state libraries.

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.

const value = useMemo(() => ({ state }), [state]);
// `dispatch` is stable, so it doesn't belong in the deps array.
return (
<MyContext.Provider value={value}>
{/* Pass dispatch via a separate context as shown above */}
{children}
</MyContext.Provider>
);

Wrap consumers in React.memo

When a context value does change, every consumer rerenders. Wrapping a consumer (or its parent) in React.memo prevents that rerender from cascading into descendants whose props haven't changed:

const Item = React.memo(function Item({ id }) {
// ...
});

This composes well with split contexts: the dispatch-only consumer doesn't rerender at all, and the state-consuming consumer rerenders but its memoized children don't.

Use the new use(Context) hook for conditional reads

React 19 added the use hook, which can read a context inside conditionals or loops. It still subscribes the component to the context, but it removes the awkward "always call useContext at the top" constraint.

import { use } from 'react';
function Avatar() {
if (someCondition) {
const theme = use(ThemeContext);
// ...
}
}

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>;
}

If your needs grow beyond what selectors give you, this is usually the point at which a dedicated state library (Zustand, Jotai, Redux Toolkit) becomes the better answer — they're built around selector-based subscriptions and avoid context's all-consumers-rerender model entirely.

Further reading

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 are largely a legacy pattern. The current React docs (React 19) discourage HOCs in favor of custom hooks for sharing logic between function components. You'll still see HOCs in older code and in libraries (e.g. connect from React Redux), but for new code, prefer hooks.


What are higher order components in React?

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.

As of React 19, the official documentation discourages HOCs for new code and recommends custom hooks instead. HOCs remain mostly relevant for compatibility with older codebases and libraries.

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:

import React from 'react';
// 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

  • 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

  • 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 deprecated 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

  • Custom hooks (preferred for new code): Custom hooks can share stateful logic between function components without adding extra wrapper components to the tree. This is the React 19 recommendation.
  • Render props: A pattern where a component uses a function as a prop to determine what to render.

Further reading

What is the Flux pattern and what are its benefits?

Topics
React

TL;DR

The Flux pattern is an architectural design Facebook introduced for managing state in React applications. It enforces a unidirectional data flow, making it easier to manage and debug application state. Today Flux is largely historical — it has been superseded by libraries it inspired, such as Redux, Zustand, and the built-in useReducer + Context combination — but its single-source-of-truth and unidirectional-flow ideas are still the foundation of those tools.

  • 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.
    • Single source of truth for application 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?

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

In 2026, you will rarely build a new app on top of the original Flux library. The pattern has been superseded by:

  • Redux — the most popular successor, which collapses Flux's many stores into a single store with pure reducer functions.
  • Zustand, Jotai, Recoil — lighter-weight stores that drop the dispatcher entirely.
  • useReducer + Context — a built-in React combination that captures the same action/reducer idea without an external library.
  • Server-state libraries like TanStack Query and SWR for data fetched from APIs.

Understanding Flux is still useful because every one of these tools borrows its core ideas: a single source of truth, immutable state transitions, and unidirectional data flow.

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.

Code example

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

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

  • Each action represents a discrete event, making it easier to trace changes in the application.
  • Stores contain pure logic, which can be unit tested independently of the view.

Scalability

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

Clear separation of concerns

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

Further reading

Explain one-way data flow of React and its benefits

Topics
React

TL;DR

In React, one-way data flow means that data moves in a single direction: from parent components down to child components via props. Children cannot mutate the props they receive; to change parent state, a child invokes a callback the parent passed in. This contrasts with two-way binding (e.g. Angular or Vue's v-model), where view and model stay in sync automatically. The main benefits are predictable state changes, easier debugging, and patterns like controlled components, immutable updates, and time-travel debugging that fall out naturally from the constraint.


One-way data flow of React and its benefits

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.

Example

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

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

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

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 updates flow upward through named callbacks, you can trace a value from where it originates to every component that consumes it. This is what makes tools like the React DevTools state inspector and Redux DevTools' time-travel debugging possible — every state transition is a discrete, replayable event rather than a side effect of a binding.

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

How do you handle asynchronous data loading in React applications?

Topics
ReactAsync

TL;DR

In a modern React app, don't roll your own useEffect + fetch for data loading — the React docs explicitly recommend against it. Reach first for a dedicated data-fetching library: TanStack Query, SWR, or RTK Query for client-side fetching, or Server Components and route-level loaders (Next.js App Router, Remix/React Router) when you control the framework. In React 19, the new use() hook lets a component read a promise directly and suspend, which pairs naturally with <Suspense> for loading states and error boundaries for failures. A hand-written useEffect+fetch is a low-level fallback that needs an AbortController, a response.ok check, and careful state handling to avoid race conditions and stale updates.


Handling asynchronous data loading in React

Prefer a data-fetching library (recommended)

The React team's recommendation in react.dev is to use a framework's built-in data fetching or a dedicated library, because they correctly handle caching, deduplication, refetching, race conditions, retries, and loading/error states — all of which are easy to get wrong by hand.

TanStack Query is the de facto standard 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.

These libraries give you for free:

  • An in-memory cache keyed by query parameters
  • Automatic deduplication of in-flight requests
  • Refetching on window focus, network reconnect, and on a stale interval
  • Cancellation via AbortSignal
  • Retries with backoff
  • Pagination, infinite scroll, and optimistic updates

Server Components and framework loaders

If you're using a framework, fetch on the server whenever possible — it's faster (no client waterfall) and ships less JS.

  • 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 user = await fetch(`https://api.example.com/users/${params.id}`).then(
(r) => r.json(),
);
return <h1>{user.name}</h1>;
}

React 19: use() + <Suspense>

React 19's use() hook lets a Client Component read a 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. The promise is typically created by a parent (often a Server Component) and passed down as a prop, so the request starts before the child renders.

'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 a user interaction triggers a re-fetch (filtering a list, switching tabs), wrap the state update that causes the new fetch in startTransition so the previous UI stays interactive while the new data loads. useDeferredValue gives you a lagging copy of a value — useful for letting an input stay snappy while a derived list re-renders against slower data.

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

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

Plain useEffect + fetch is a fine fallback for one-off fetches in a small app, or as an escape hatch. But you must handle two things the naive example always gets wrong: 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 {
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 does not give you caching across components, deduplication, or refetch-on-focus — which is why a real app outgrows it quickly.

Summary of options

OptionUse when
TanStack Query / SWR / RTK QueryDefault for client-side fetching
Server Components / route loadersYou control the framework (Next.js, Remix)
use(promise) + <Suspense>Streaming a promise from a parent in React 19+
useEffect + fetchOne-off fetches, learning, or escape hatch only

Further reading

Explain server-side rendering of React applications and its benefits

Topics
React

TL;DR

Server-side rendering (SSR) in React involves rendering React components on the server and sending the resulting HTML to the client. The browser displays that HTML immediately, then hydrateRoot attaches event handlers so the page becomes interactive. Modern React supports streaming SSR via renderToPipeableStream (Node) and renderToReadableStream (Web), and React Server Components let parts of the tree render only on the server. Benefits include faster perceived loads and better SEO; tradeoffs include a slower TTFB, the cost of hydration, and the risk of hydration mismatches.


What is server-side rendering of React applications?

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

  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.

Streaming SSR and React Server Components

Modern React provides two streaming server APIs:

  • renderToPipeableStream for Node.js streams.
  • renderToReadableStream for Web Streams (used in edge and worker runtimes).

Both let you wrap parts of the tree in <Suspense> so the server can flush the shell first and stream the slow parts in as their data resolves.

React Server Components (RSC) take this further: components marked as server-only never ship to the client and can fetch data directly. Components that need state, effects, or browser APIs are marked with the 'use client' directive at the top of the file, defining a "client boundary". Frameworks like Next.js (App Router) and Remix build on these primitives.

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 per-request rendering (SSR). Omit for static generation.
cache: 'no-store',
});
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 the HTML produced on the server does not match what React renders on the client during hydration — for example, rendering new Date().toLocaleString() or Math.random() on both sides, or branching on window. React 19 logs detailed diffs and recovers by re-rendering the mismatched subtree on the client; the fix is usually to gate browser-only output behind useEffect or a <ClientOnly> boundary.

Benefits of server-side rendering

Improved initial load time

  • Faster content display: Since the server sends fully rendered HTML, users see the content faster compared to CSR, where the browser has to download and execute JavaScript before rendering.

Better SEO

  • Search engine indexing: Search engines can easily index the fully rendered HTML, improving the SEO of your application. This is particularly important for content-heavy sites.

Performance on slower devices

  • Less client-side rendering work: SSR (and especially RSC) shifts work to the server, so low-powered devices have less HTML to construct from JavaScript. The JS still has to download and hydrate, but the first paint does not depend on it.

Tradeoffs to be aware of

  • 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

Explain static generation of React applications and its benefits

Topics
React

TL;DR

Static generation (SSG) pre-renders pages to HTML at build time, instead of rendering them per request. The output is plain files that can be served from a CDN, which makes loads fast and SEO straightforward. Frameworks like Next.js, Remix, Astro, and Gatsby support it; in Next.js's App Router, fetches are statically generated by default and generateStaticParams enumerates dynamic routes at build. Incremental Static Regeneration (ISR) lets you re-build individual pages in the background after a TTL, so static does not have to mean stale. SSG is best for content that does not vary per user and does not need to be perfectly fresh.


Static generation of React applications and its benefits

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?

  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.

Benefits of static generation

Improved performance
  • Faster load times: Since the HTML is pre-generated, it can be served immediately without waiting for server-side rendering.
  • Reduced server load: Static files can be served from a CDN, reducing the load on the origin server.
Better SEO
  • Search engine indexing: Pre-rendered HTML is trivially indexable — crawlers do not need to execute JavaScript to see the content.
  • Consistent content: Every request returns the same HTML, so what crawlers see matches what users see.
Scalability
  • 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, server components are statically rendered by default unless you opt into dynamic behavior. For dynamic routes, generateStaticParams enumerates the params to pre-render at build time.

// app/posts/[slug]/page.jsx
async function getPost(slug) {
const res = await fetch(`https://api.example.com/posts/${slug}`);
return res.json();
}
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((r) =>
r.json(),
);
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }) {
const post = await getPost(params.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.

Incremental Static Regeneration (ISR)

ISR lets you keep the speed of static while still picking up content updates. You declare a revalidation window (in seconds) and the framework serves the cached HTML; after the window expires, the next request triggers a background re-render and subsequent requests get the fresh version.

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

You can also revalidate on demand (e.g. from a webhook) using revalidatePath or revalidateTag, which is useful for CMS-driven content.

When SSG is not a good fit

  • 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 generateStaticParams would emit millions of pages, build time and storage become a problem. Mitigations include partial pre-rendering (build the popular pages, defer the rest to on-demand SSR/ISR) or moving to SSR.
  • Stale-data tradeoffs: Plain SSG serves whatever was true at build time until the next deploy. ISR shortens that window but always serves the previous version on the request that triggers revalidation, so users may see slightly stale content.

Further reading

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

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
  • 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
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
  • 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 = () => {
// Handle button click
};
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, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { fetchData } from './actions';
import Button from './Button';
function useButtonContainer() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchData());
}, [dispatch]);
const handleClick = useCallback(() => {
// Handle button click
}, []);
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

  • 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

What are some common pitfalls when doing data fetching in React?

Topics
React

TL;DR

Common pitfalls when doing data fetching in React include not handling loading and error states, leaking requests by not aborting them on unmount, ignoring race conditions when props or query params change, fetching during render (which loops), and triggering request waterfalls. In modern React (18+), use AbortController for cleanup, account for StrictMode's intentional double-invoke in development, and prefer purpose-built libraries like TanStack Query, SWR, or RTK Query for caching and deduplication. React 19's use() hook plus Suspense, and Server Components, are now the recommended way to read promises in components.


Common pitfalls when doing data fetching in React

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(() => {
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => {
setData(data);
setLoading(false);
})
.catch((error) => {
setError(error);
setLoading(false);
});
}, []);
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();
fetch('https://api.example.com/data', { signal: controller.signal })
.then((response) => response.json())
.then((data) => setData(data))
.catch((error) => {
if (error.name !== 'AbortError') setError(error);
});
return () => controller.abort();
}, []);
// ...
}

The older let isMounted = true flag pattern still works, but it doesn't cancel the network request and doesn't help with race conditions — prefer AbortController.

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. Aborting the previous request on cleanup handles this for free:

import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((response) => response.json())
.then((data) => setUser(data))
.catch((error) => {
if (error.name !== 'AbortError') console.error(error);
});
return () => controller.abort();
}, [userId]);
// ...
}

Forgetting StrictMode's intentional double-invoke

In development with <StrictMode>, React intentionally mounts, unmounts, and remounts every component, which fires effects twice. If your fetch has visible side effects (logging, analytics, POSTs) you may notice duplicates in dev — this is a hint that the effect needs proper cleanup, not a bug to suppress.

Fetching data during render

Calling fetch directly in the render body kicks off a new request on every render. If the response then triggers setState, you get an infinite re-render loop. Note that fetch().then() returns a Promise, not the resolved value — JSON.stringify(promise) will not give you what you expect.

// 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
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch('https://api.example.com/data', { signal: controller.signal })
.then((response) => response.json())
.then((data) => setData(data));
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 usually means a fetch loop.

// Incorrect — uses `userId` but doesn't list it; data goes stale.
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((r) => r.json())
.then(setUser);
}, []);
// Correct
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.

Not using React 19's use() and Suspense

In React 19, you can read a promise directly with use() and let Suspense handle the loading state. Combined with Server Components or a framework that passes promises down, this removes most of the manual useState/useEffect/loading-flag boilerplate:

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>
);
}

For most apps, the right place to fetch is a React Server Component (in a framework like Next.js) or a route loader — the data lives close to where it's rendered, and the client never has to coordinate the request itself.

Further reading

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 { useEffect, useState } from 'react';
function DataFetcher({ url, children }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then((response) => response.json())
.then(setData);
}, [url]);
return children(data);
}
// Usage
<DataFetcher url="/api/data">
{(data) => <div>{data ? data.name : 'Loading...'}</div>}
</DataFetcher>;

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?

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

  • 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

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

Mutating state directly

Directly mutating state doesn't tell React anything has changed, so it won't re-render. Always produce a new value (object, array) 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, no re-render guaranteed
// 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, wrap it in useMemo. The React Compiler can also handle this for you.

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 — depend on `count`, or use a functional updater
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 in <StrictMode> the dev-time double-mount makes the leak immediately visible (two intervals, two listeners).

// 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 is unsafe — render must be pure, and React 19's strictness flags this. Read/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 ref = useRef(0);
useEffect(() => {
ref.current += 1;
});
return <div>{ref.current}</div>;
}

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 only matters when the receiving child is wrapped in React.memo and the new reference defeats the memoization. With the React Compiler enabled, even those cases are usually handled for you.

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.

The React Compiler (RC/stable as of 2026) handles most of this automatically, making manual memoization unnecessary in many codebases.

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

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. Use React Context to pass values that change rarely (theme, locale, current user) — Context is not a state manager and re-renders all consumers on every change. Reach for a client-state library like Zustand, Jotai, or Redux Toolkit when many unrelated components need to share frequently-changing state. Critically, treat server state separately: TanStack Query, SWR, or RTK Query handle caching, refetching, and invalidation far better than any general-purpose store.


Deciding between React state, context, and external state managers

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
  • 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
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: [] });
// ...
}

React Context

Context is a dependency-injection mechanism, not a state manager. It lets you read a value from anywhere in the tree without prop-drilling, but it does not optimize re-renders: every consumer re-renders whenever the provider's value changes (by Object.is). Putting frequently-changing state into a single context turns it into a global re-render bus.

Use Context for values that change rarely or only at well-defined boundaries — theme, locale, current user, feature flags — and pair it with useReducer for slow-moving app state. For high-frequency state shared across the tree, prefer a real store (Zustand, Jotai, Redux Toolkit).

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, useState } from 'react';
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// React 19: <Context> works as a Provider directly
return <ThemeContext value={{ theme, setTheme }}>{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
  • 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
  • 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 gives you fine-grained subscriptions (only consumers of a changed slice re-render), middleware, devtools, and persistence.

  • Zustand — minimal, hook-based store, no provider required. Great default for most apps.
  • Jotai — atomic, bottom-up model where each piece of state is an atom; consumers only re-render when an atom they read changes.
  • 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 actively maintained by Meta — new projects should pick Jotai or Zustand instead.
When to use an external store
  • 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
// 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
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.

Use a dedicated server-state library for anything you fetch:

  • TanStack Query (formerly React Query) — the de facto standard.
  • SWR — lightweight alternative from Vercel.
  • RTK Query — built into Redux Toolkit, integrates with the same store.

This typically shrinks your client store dramatically: most "global state" turns out to be server state in disguise.

import { useQuery } from '@tanstack/react-query';
function Profile({ id }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', id],
queryFn: () => fetch(`/api/users/${id}`).then((r) => r.json()),
});
if (isLoading) return <Spinner />;
if (error) return <Error error={error} />;
return <h1>{data.name}</h1>;
}

Quick decision guide

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

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

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

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 Radix UI and Headless UI, and by HTML itself (<select> with <option>).

Benefits of composition

  • 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

  • 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

What is React? Describe the benefits of React