Built-in Components Reference

Besides the components you write yourself, React ships a handful of special built-in components that don’t render any HTML of their own. Instead, they change how React treats the tree beneath them — grouping elements without adding a DOM node, catching bugs in development, showing a fallback while data loads, rendering children into a different part of the DOM, or measuring render performance. Knowing these well lets you write cleaner markup, catch bugs earlier, and build features like modals and code-split routes correctly.

Overview / How it works

Every built-in component in this lesson is imported from either react or react-dom, and each one wraps part of your tree to change React’s behavior for that subtree, without itself becoming a real DOM element:

  • <Fragment> (usually written as <></>) groups a list of children so a component can return multiple elements without adding an extra wrapper <div> to the DOM.
  • <StrictMode> adds development-only checks. It doesn’t render anything visible; it makes React intentionally double-invoke certain functions (component bodies, state updater functions, effect setup/cleanup) so that impure code surfaces as a bug during development instead of production.
  • <Suspense> lets a component “wait” for something — most commonly a lazily-loaded component via React.lazy — and shows a fallback UI until it’s ready.
  • <Profiler> measures how often a part of your tree renders and how expensive those renders are, calling an onRender callback with timing data.
  • createPortal (a function from react-dom, not a JSX component itself, but used constantly alongside these) renders children into a DOM node outside the normal parent-child hierarchy — essential for modals, tooltips, and dropdowns that must escape a parent’s overflow: hidden or z-index stacking context.

Under the hood, React’s renderer walks your element tree and decides what real DOM nodes to create. Fragments and Suspense boundaries are markers in that tree that the renderer understands specially — a Fragment tells the reconciler “these children belong together, but don’t wrap them in a host element,” while a Suspense boundary tells it “if anything inside throws a promise (is still loading), render the fallback here instead, and swap it back in when the promise resolves.” StrictMode doesn’t touch the DOM at all — it only changes how many times React calls your functions in development, which is why it never affects the built app users download.

Syntax

Component / API Import from Purpose
<Fragment> / <></> react Group children without adding a DOM node
<StrictMode> react Enable extra development-only checks and warnings
<Suspense fallback={...}> react Show a fallback while lazy children or data are loading
<Profiler id onRender> react Measure render timing of a subtree
createPortal(children, domNode) react-dom Render children into a different DOM node

Each of these except createPortal is used as a JSX wrapper around other elements:

<StrictMode>
  <Suspense fallback={<p>Loading...</p>}>
    <App />
  </Suspense>
</StrictMode>

Examples

Example 1: Fragment to avoid an extra wrapper

function NameFields() {
  return (
    <>
      <label htmlFor="first">First name</label>
      <input id="first" name="first" />
      <label htmlFor="last">Last name</label>
      <input id="last" name="last" />
    </>
  );
}

Renders: two label/input pairs directly into the parent, with no extra <div> wrapping them in the DOM. This matters when the parent is something like a <table> row or a CSS grid, where an unexpected wrapper element would break the layout or produce invalid HTML.

When you need a key (for example, returning a fragment from inside a .map()), you must use the explicit <Fragment> form, because the <></> shorthand cannot accept props:

import { Fragment } from "react";

function Glossary({ terms }) {
  return (
    <dl>
      {terms.map((t) => (
        <Fragment key={t.id}>
          <dt>{t.term}</dt>
          <dd>{t.definition}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

This renders a definition list where each term/definition pair is grouped by a stable key, without any extra DOM element between the <dt>/<dd> pairs and their <dl> parent.

Example 2: StrictMode at the application root

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

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

Does: in development only, React intentionally renders each component’s function body twice, and runs each effect’s setup and cleanup twice on mount, to help you notice code that isn’t “pure” (for example, an effect that subscribes without unsubscribing, or a component body with a side effect that shouldn’t be there). In production builds this extra work is stripped out entirely, so it never affects real users or performance.

Example 3: Suspense with a lazily-loaded component

import { Suspense, lazy } from "react";

const Dashboard = lazy(() => import("./Dashboard"));

function App() {
  return (
    <Suspense fallback={<p>Loading dashboard...</p>}>
      <Dashboard />
    </Suspense>
  );
}

Renders: “Loading dashboard…” immediately, then swaps in the real <Dashboard /> once its JavaScript chunk has finished downloading and evaluating. React.lazy returns a component that “suspends” (throws a promise internally) while its module is loading; the nearest enclosing <Suspense> catches that and shows fallback until the promise resolves. This is the standard way to code-split a React app by route or by feature.

Example 4: Portal for a modal

import { createPortal } from "react-dom";

function Modal({ children, onClose }) {
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.getElementById("modal-root")
  );
}

Does: renders the overlay and content into the DOM node with id modal-root (typically a sibling of the app’s root <div>, declared in index.html), even though <Modal /> is used deep inside the React tree. Event bubbling still works through React’s synthetic event system as if the modal were nested normally, so a click handler on an ancestor React component still fires — only the DOM placement changes. This is the standard fix for modals getting visually clipped by a parent’s overflow: hidden or stacked behind other elements by z-index.

How it works step by step / Under the hood

  • Fragment: on render, React sees a Fragment node in the element tree and skips creating a host DOM element for it, attaching its children directly to the nearest real parent DOM node.
  • StrictMode (development only): on mount, React calls the wrapped components’ render functions twice (throwing away one set of results), and runs effect setup, then cleanup, then setup again, so any effect that isn’t properly idempotent shows an observable symptom immediately rather than in production weeks later.
  • Suspense (mount): React starts rendering the children; if a lazy component’s module isn’t loaded yet, rendering “suspends,” and React commits the fallback to the DOM instead. When the import resolves, React re-renders the children and swaps the fallback out for the real content.
  • Suspense (update): if new suspending content appears inside an already-mounted Suspense boundary, React can keep showing the old content while the new content loads (depending on how the update was triggered), then commit the swap once ready.
  • Profiler: on every commit of the wrapped subtree, React calls onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) so you can log or aggregate render cost.
  • Portal (unmount): when the component calling createPortal unmounts, React removes the portaled DOM node’s contents just like any other unmounting subtree — you don’t need to manually clean up the target element’s children.

Common Mistakes

Mistake 1: Wrapping a table row’s cells in a real element instead of a Fragment.

function Row({ item }) {
  return (
    <div>
      <td>{item.name}</td>
      <td>{item.price}</td>
    </div>
  );
}

A <div> is not valid inside a <tr>, so the browser silently “corrects” the invalid HTML, breaking the table layout. Use a Fragment, which adds no element at all:

function Row({ item }) {
  return (
    <>
      <td>{item.name}</td>
      <td>{item.price}</td>
    </>
  );
}

Mistake 2: Forgetting the fallback prop, or putting non-suspending logic inside Suspense and expecting it to catch errors. <Suspense> without a fallback renders nothing while loading (usually not what you want, and easy to mistake for a bug), and Suspense only catches components that actually suspend (like React.lazy or Suspense-enabled data fetching) — it does not catch thrown errors. For error handling you need a separate error boundary, not <Suspense>.

<Suspense>
  <LazyWidget />
</Suspense>

Always give it a real fallback:

<Suspense fallback={<Spinner />}>
  <LazyWidget />
</Suspense>

Mistake 3: Assuming StrictMode’s double-render means something is broken. Seeing a component body or an effect run twice in development under <StrictMode> is expected behavior, not a bug in React. If doubled console logs or doubled network requests surprise you, it usually means the code has a side effect that isn’t safely repeatable — that’s StrictMode doing its job by surfacing it early.

Best Practices

  • Prefer the <></> shorthand for Fragments; only reach for the explicit <Fragment key={...}> form when you need to pass a key (typically inside a list).
  • Wrap your entire app in <StrictMode> in development — it costs nothing in production and catches impure renders and effects early.
  • Always pass a meaningful fallback to <Suspense>, and place boundaries thoughtfully — one per route or per major section is usually better than one giant boundary around the whole app.
  • Use createPortal for anything that must visually escape its parent’s clipping or stacking context: modals, toasts, tooltips, and dropdown menus.
  • Only wrap <Profiler> around subtrees you’re actively investigating for performance — it adds overhead and is meant as a diagnostic tool, not something to leave permanently around your whole app.
  • Remember a portal changes only where a component renders in the DOM — its position in the React tree (and therefore context, event bubbling, and state) is unchanged.

Practice Exercises

  • Refactor a component that currently returns a single wrapping <div> containing a heading and a paragraph so it returns a Fragment instead, and confirm (by inspecting the rendered HTML) that the extra <div> is gone.
  • Build a <Tooltip> component that uses createPortal to render its content into a document.getElementById("tooltip-root") node, so it can visually overflow a parent card that has overflow: hidden.
  • Take a component that fetches and displays a list of posts, split it out with React.lazy, and wrap it in <Suspense fallback={<p>Loading posts...</p>}>. Verify the fallback briefly appears on a slow network (use your browser dev tools’ network throttling).

Summary

  • <Fragment> (<></>) groups elements without adding an extra DOM node; use the explicit form when a key is needed.
  • <StrictMode> is a development-only helper that double-invokes renders and effects to surface impure code; it has zero effect on production builds.
  • <Suspense fallback={...}> shows fallback UI while lazily-loaded components (or Suspense-enabled data sources) are still loading.
  • <Profiler id onRender> measures render timing for a subtree — a diagnostic tool, not something to ship permanently.
  • createPortal(children, domNode) from react-dom renders children into a different part of the DOM while keeping normal React context and event bubbling.
  • None of these built-ins render visible DOM themselves (except the portal’s children, which render where you point them) — they all change React’s behavior around the elements you already write.