root.render()

Displays a React node inside a root, updating the existing tree when called again.

Signature
root.render(reactNode)
  • Added in React 18.0.0

Parameters

ParameterTypeDescription
$reactNodeReact nodeThe node to display — usually JSX such as <App />, but also an element from createElement(), a string, a number, null, or undefined.

Return value

undefinedundefined.

Errors

Throws "Cannot update an unmounted root" if called after root.unmount().

Changelog

  • PHP 18.0.0Replaced the callback argument of the old ReactDOM.render(element, container, callback) API; there is no third parameter, so use a ref callback or useEffect to run code after the render commits.

root.render is how a value gets from outside React into a React tree. The first call clears whatever HTML was already in the container; every later call on the same root is treated as an update, not a fresh mount.

When to reach for it

  • Once, in your entry file, to display the app.
  • Repeatedly, when non-React code owns a piece of state and needs to push it in — a legacy page that calls root.render(<Widget user={user} />) whenever its own store changes.

That second pattern is safe precisely because React diffs: if the element type at each position is unchanged, component state and DOM nodes survive the call. A counter inside the tree keeps its value; only the props you changed are applied.

What trips people up

The call is asynchronous in the part that matters. Rendering starts synchronously, but code written on the next line can run before that render's useLayoutEffect and useEffect have fired, so reading the DOM straight after root.render may show the previous frame. Wrap the call in flushSync only if you truly need the effects flushed first.

There is also no completion callback — the old third argument of ReactDOM.render is gone. Use a ref callback or an effect inside the tree instead.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Mounting the application once from the entry file
  • Pushing fresh props into a React widget whenever a non-React store outside it changes
  • Swapping the root element between a loading placeholder and the real app
  • Clearing a root's contents without tearing it down by rendering null

Frequently asked questions

Does calling root.render twice remount the component?

No. The second call is an update: React diffs the new element against the current tree and keeps component state and DOM nodes wherever the element type matches. Use a different key on the top-level element if you want a real remount.

How do I run code after root.render finishes?

There is no callback argument any more — use a useEffect inside the rendered tree, or a ref callback on the element you need. If you must block until the DOM is updated, wrap root.render in flushSync from react-dom.

Why is my container empty right after root.render?

Because root.render is not fully synchronous: the DOM and the render's effects may not be committed by the time the next line runs. Read the DOM from an effect inside the tree, or wrap the call in flushSync.

More examples

Driving a React widget from a non-React store

Every store change calls root.render with a new element. Because the element type is always CartBadge, React updates the existing DOM text instead of replacing the span, so the node identity, focus, and any internal state survive. This is the supported way to bridge a React island into an app whose state lives elsewhere.

Example · jsx
import { createRoot } from "react-dom/client";

function CartBadge({ count, currency }) {
  return (
    <span>
      {count} item{count === 1 ? "" : "s"} · {currency}
    </span>
  );
}

const root = createRoot(document.getElementById("cart-badge"));

// legacyStore is plain JS owned by the surrounding page.
function paint() {
  root.render(
    <CartBadge count={legacyStore.count} currency={legacyStore.currency} />
  );
}

legacyStore.subscribe(paint);
paint();

Rendering null to empty a root without destroying it

root.render(null) unmounts the current children and leaves the container empty, yet the root stays alive, so showToast can render into it again later. Contrast this with root.unmount(), after which any further render call throws.

Example · jsx
import { createRoot } from "react-dom/client";

function Toast({ message }) {
  return <div role="status">{message}</div>;
}

const root = createRoot(document.getElementById("toast-host"));

export function showToast(message) {
  root.render(<Toast message={message} />);
}

export function hideToast() {
  // Container goes empty, but the root is still usable.
  root.render(null);
}

Reading the DOM after a render

The first log can print 0: root.render schedules the work and the effects of that render have not run yet. Wrapping the call in flushSync forces React to complete the render and flush its effects before flushSync returns, so the query sees three list items. Reach for flushSync only for this kind of measurement — it opts that update out of concurrent rendering.

Example · jsx
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";

function Rows({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

const host = document.getElementById("list");
const root = createRoot(host);

root.render(<Rows items={["a", "b", "c"]} />);
console.log(host.querySelectorAll("li").length); // may still be 0

flushSync(() => {
  root.render(<Rows items={["a", "b", "c"]} />);
});
console.log(host.querySelectorAll("li").length); // 3

Discussion

  • Be the first to comment on this lesson.