Home / Articles / React Fragments and StrictMode: Leaner Markup and Earlier Bug Detection

This article is published in English.

React Fragments and StrictMode: Leaner Markup and Earlier Bug Detection

Learn how React Fragments group elements without extra DOM wrappers and how StrictMode's development-only double invocation exposes impure renders and effects.

976 words

Two React features never draw anything on screen yet shape almost every component tree: Fragments and StrictMode. Fragments keep your DOM free of wrapper elements that exist only to satisfy JSX, while StrictMode deliberately stresses your components in development so that impure logic shows up before users do.

Why JSX needs a single root

JSX compiles each tag into a function call, so two sibling tags returned together are two values where one is expected. This fails to compile:

return (
  <h1>Hello</h1>
  <p>Welcome</p>
);

The traditional workaround was to wrap the siblings in a div:

return (
  <div>
    <h1>Hello</h1>
    <p>Welcome</p>
  </div>
);

The extra node has costs: it deepens the DOM, breaks flex or grid layouts that expect direct children, complicates selectors, and can yield invalid or less accessible HTML.

Grouping without a wrapper

A Fragment groups children for React without producing any DOM element. The short syntax is an empty tag pair:

return (
  <>
    <h1>Hello</h1>
    <p>Welcome</p>
  </>
);

The explicit form, React.Fragment, does the same thing and is required when you need to pass a prop:

return (
  <React.Fragment>
    <h1>Hello</h1>
    <p>Welcome</p>
  </React.Fragment>
);

The rendered HTML contains only the h1 and the p. The performance gain is tiny; the real benefit is correct markup.

Returning sibling elements

A component that produces several peers can leave layout to its parent:

function Card() {
  return (
    <>
      <h2>Title</h2>
      <p>Description</p>
    </>
  );
}

Keyed Fragments in lists

When mapping over data where each item yields more than one element, React still needs a stable key per item. The short <> syntax cannot accept attributes, so use React.Fragment with a key:

items.map(item => (
  <React.Fragment key={item.id}>
    <h2>{item.title}</h2>
    <p>{item.description}</p>
  </React.Fragment>
));

Table cells and rows

HTML tables have a strict content model: a tr may only contain td or th. A wrapping div is invalid there. A Fragment lets a component contribute several cells to a row owned by its parent:

function Row() {
  return (
    <>
      <td>A</td>
      <td>B</td>
    </>
  );
}

What StrictMode checks

StrictMode renders nothing, adds no element, and has no effect in production builds. In development it enables extra checks, including:

  • warnings for legacy class lifecycle methods considered unsafe, such as componentWillMount
  • warnings for deprecated APIs such as string refs and findDOMNode
  • calling component bodies, initialisers and updater functions twice to surface impure rendering
  • running effects through an extra setup, cleanup and setup cycle on mount to surface missing cleanup

The exact list has changed across React versions, so check the current documentation for your version.

Intentional double rendering

Consider a component that logs as it renders:

function App() {
  console.log("Rendered!");
  return <h1>Hello</h1>;
}

Under StrictMode in development, the console shows the message twice:

Rendered!
Rendered!

This is by design. A render function should be pure: given the same props and state, it returns the same output and changes nothing outside itself. Calling it twice makes violations, such as mutating a shared variable, produce visibly wrong results. Purity matters because concurrent rendering may start, pause, discard or repeat renders, and code that assumes exactly one render per update breaks under those conditions.

Turning it on

Wrap the tree you want checked, usually the whole app, at the root:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Effects under StrictMode: the dependency array is not the fix

Here is an effect with no dependency array. It runs after every render, so each update triggers another request:

useEffect(() => {
  console.log("Fetching...");
  fetch("/api");
});

Adding an empty array limits the effect to mount:

useEffect(() => {
  console.log("Fetching...");
  fetch("/api");
}, []); // stable dependency

That change is correct, but it does not stop the double request in development. Since React 18, StrictMode mounts the component, runs its cleanup, and mounts it again, so an effect with [] still runs twice there. The double run is the signal, not the bug. The real fix is a cleanup function that makes a second run harmless, typically aborting the first request with an AbortController or ignoring its result with a flag. Such an effect is also safe against real remounts in production.

Side by side

  • Purpose: Fragments group elements; StrictMode surfaces bugs.
  • DOM output: neither adds an element.
  • Production impact: none for either.
  • Runtime behaviour: Fragments render normally; StrictMode double-invokes renders and effects in development only.
  • What you gain: cleaner, valid markup versus predictable, side-effect-safe code.

A minimal setup to try

The component below returns two siblings through a Fragment:

export default function App() {
  return (
    <>
      <h1>Hello World</h1>
      <p>Rendered using Fragments</p>
    </>
  );
}

The entry file renders it inside StrictMode, so any impure logic you add later is caught immediately in development:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Key takeaways

  • Reach for a Fragment whenever a wrapper element would exist only to satisfy JSX, and use React.Fragment with a key in lists.
  • Keep StrictMode enabled in development; double logs and double effects are intentional diagnostics.
  • Fix double-running effects with proper cleanup, not by fiddling with dependencies.