React DOM

React is primarily responsible for building user interfaces, but something still needs to connect those React components to the actual web page in the browser. This is where React DOM comes in.

React DOM provides APIs that allow React applications to work with the browser’s Document Object Model (DOM). It helps React create and update the user interface, mount React applications into HTML elements, remove applications from the page, render React content into different parts of the DOM, and work with server-rendered HTML.

In modern React, most applications start with createRoot() and root.render(). Other APIs such as hydrateRoot(), createPortal(), and flushSync() are used for specific situations.

By the end of this chapter, you will understand:

  • What React DOM is
  • How createRoot() works
  • How root.render() displays React components
  • How root.unmount() removes a React application
  • When hydrateRoot() is used
  • How createPortal() renders content elsewhere in the DOM
  • What flushSync() does and why it should be used carefully

What is React DOM?

React DOM is the React package that provides APIs for integrating React with the browser’s DOM.

React itself focuses on describing what the user interface should look like. React DOM provides the browser-specific functionality needed to put that interface into an actual HTML document.

For example, suppose your HTML contains:

<div id="root"></div>

You can create a React application and display it inside that element:

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

function App() {
  return <h1>Hello, React!</h1>;
}

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(<App />);

The browser initially has:

<div id="root"></div>

After React renders the application, the page displays:

<div id="root">
  <h1>Hello, React!</h1>
</div>

React manages the React content inside the root.

React DOM vs React

It is useful to understand the difference between the react package and react-dom.

The react package provides the core React functionality used to create components, state, Hooks, Context, and other React features.

For example:

import { useState } from "react";

React DOM provides APIs specifically for connecting React with the browser DOM.

For example:

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

A typical browser application may therefore use both:

import { useState } from "react";
import { createRoot } from "react-dom/client";

Conceptually:

PackageMain responsibility
reactComponents and React functionality
react-domConnecting React to the browser DOM
react-dom/clientClient-side rendering and hydration APIs
react-dom/serverRendering React on the server

For most modern client-side React applications, you will frequently see createRoot imported from react-dom/client.

createRoot

The createRoot() API creates a React root inside a DOM element.

A root is the entry point where React manages a portion of the page.

The basic syntax is:

const root = createRoot(container);

For example:

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

function App() {
  return <h1>Welcome to Web4Learners</h1>;
}

const container = document.getElementById("root");

const root = createRoot(container);

At this point, React has created a root, but the application has not yet been displayed.

You still need to call:

root.render(<App />);

The complete example

A typical React entry file looks like this:

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

const container = document.getElementById("root");

const root = createRoot(container);

root.render(<App />);

The process can be understood as:

HTML element
     ↓
createRoot()
     ↓
React Root
     ↓
root.render()
     ↓
React Application
     ↓
Browser UI

Why do we need a root?

React needs to know which part of the HTML document it is responsible for managing.

Consider:

<body>
  <header>My Website</header>

  <div id="root"></div>

  <footer>Copyright 2026</footer>
</body>

If you call:

createRoot(document.getElementById("root"));

React manages the content inside #root.

The header and footer remain outside that React root.

This allows React to control a specific part of a larger web page.

The root container

The element passed to createRoot() is called the root container.

For example:

const container = document.getElementById("root");

const root = createRoot(container);

Here:

  • container refers to the actual DOM element.
  • root represents React’s root.
  • React manages the React tree rendered into that root.

createRoot() does not render by itself

This is an important beginner concept.

This:

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

creates the root.

It does not mean your component has been rendered yet.

You must also call:

root.render(<App />);

root.render

The root.render() method tells React what should be displayed inside the root.

For example:

root.render(<App />);

You can render a component:

root.render(<App />);

Or an element:

root.render(<h1>Hello React</h1>);

You can also render a component with props:

root.render(<Profile name="Karim" />);

A complete example

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

function App() {
  return (
    <main>
      <h1>React DOM</h1>
      <p>Welcome to Web4Learners.</p>
    </main>
  );
}

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

root.render(<App />);

React creates the UI represented by <App /> and manages it inside the root.

Rendering a different component

A root can render a different React tree later.

For example:

function Home() {
  return <h1>Home Page</h1>;
}

function Login() {
  return <h1>Login Page</h1>;
}

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

root.render(<Home />);

// Later
root.render(<Login />);

React updates the existing root instead of creating another React root.

Don’t repeatedly call createRoot()

A common mistake is doing this:

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

This works as a compact entry-point expression, but you should not repeatedly call createRoot() for the same DOM element.

Instead, create the root once:

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

root.render(<App />);

Then reuse that root if you need to render another tree.

React rendering does not mean rebuilding the entire DOM

When you call:

root.render(<App />);

React does not simply throw away the entire page and recreate every DOM element whenever something changes.

React compares the new React tree with the previous one and applies the necessary DOM updates.

For example, if only a counter value changes, React can update the relevant text in the DOM rather than rebuilding the entire page.

This is one reason understanding React rendering and reconciliation is important.

root.unmount

The root.unmount() method removes a React root from the DOM and tells React to stop managing that root.

For example:

root.unmount();

After unmounting, React no longer manages the React tree associated with that root.

Why would you unmount a React application?

Most React applications stay mounted for the entire lifetime of the page.

However, unmounting can be useful when React is being used inside a larger application or when a section of the page is dynamically created and removed.

For example:

const container = document.getElementById("widget");

const root = createRoot(container);

root.render(<Widget />);

Later:

root.unmount();

This tells React that the application is no longer needed.

Unmounting and cleanup

Unmounting is also important because React can clean up resources associated with the component tree.

For example:

import { useEffect } from "react";

function ChatWidget() {
  useEffect(() => {
    const connection = connectToChat();

    return () => {
      connection.disconnect();
    };
  }, []);

  return <div>Chat</div>;
}

When the root is unmounted:

root.unmount();

React runs the appropriate Effect cleanup.

This is important when components use:

  • Event listeners
  • Timers
  • Subscriptions
  • WebSocket connections
  • External libraries
  • Other resources requiring cleanup

Example

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

root.render(<App />);

// Later
root.unmount();

After unmount(), React has disconnected itself from that root.

hydrateRoot

hydrateRoot() is used when React needs to take over HTML that was already rendered on the server.

This is especially important in applications that use server-side rendering.

For example, a server might send HTML like:

<div id="root">
  <h1>Welcome</h1>
</div>

The browser already has the HTML.

Instead of creating the entire UI from scratch, React can hydrate the existing HTML.

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

hydrateRoot(
  document.getElementById("root"),
  <App />
);

What does hydration mean?

Hydration is the process where React connects its client-side behavior to HTML that was previously generated on the server.

Think of server rendering as creating the initial HTML:

Server
   ↓
HTML
   ↓
Browser

Hydration then allows React to take over that existing HTML:

Existing HTML
      ↓
hydrateRoot()
      ↓
React attaches behavior
      ↓
Interactive application

Server rendering vs client rendering

With normal client-side rendering:

const root = createRoot(container);

root.render(<App />);

React creates and manages the application from the client side.

With server rendering, HTML may already exist when JavaScript loads.

In that situation:

hydrateRoot(container, <App />);

allows React to use the existing HTML.

Why not use createRoot() for server-rendered HTML?

If HTML was generated by React on the server and you want to make that existing HTML interactive, you generally use:

hydrateRoot()

rather than:

createRoot()

The purpose is different.

APIMain purpose
createRoot()Start a client-rendered React application
hydrateRoot()Attach React to server-rendered HTML

Hydration must match the server output

The server-generated HTML and the initial client render should represent the same UI.

For example, if the server renders:

<h1>Hello</h1>

but the client immediately expects:

<h1>Goodbye</h1>

React may report a hydration mismatch.

This is why developers need to be careful with values that can differ between server and browser, such as certain browser-only information or uncontrolled randomness.

createPortal

Normally, when a component renders JSX, its DOM elements appear inside the DOM hierarchy where the component is rendered.

A portal allows React to render a piece of UI into a different DOM location while keeping it part of the same React tree.

The API is:

createPortal(children, domNode)

It is imported from react-dom:

import { createPortal } from "react-dom";

Basic example

Suppose your HTML contains:

<div id="root"></div>
<div id="modal-root"></div>

Your React application can render a modal into modal-root:

import { createPortal } from "react-dom";

function Modal() {
  return createPortal(
    <div className="modal">
      <h2>Hello</h2>
      <button>Close</button>
    </div>,
    document.getElementById("modal-root")
  );
}

The modal is logically part of the React component tree, but its DOM nodes are placed inside:

<div id="modal-root"></div>

Why are portals useful?

Portals are particularly useful for UI that needs to visually escape its normal container.

Common examples include:

  • Modals
  • Dialogs
  • Tooltips
  • Dropdown menus
  • Toast notifications
  • Popovers
  • Full-screen overlays

For example, a modal inside a container with:

overflow: hidden;

may have layout problems.

A portal allows the modal to be rendered somewhere else in the DOM where it can be positioned correctly.

Portal example with state

import { useState } from "react";
import { createPortal } from "react-dom";

function Modal({ onClose }) {
  return createPortal(
    <div className="modal-backdrop">
      <div className="modal">
        <h2>Delete Account</h2>
        <p>Are you sure you want to continue?</p>

        <button onClick={onClose}>
          Cancel
        </button>
      </div>
    </div>,
    document.getElementById("modal-root")
  );
}

function App() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>
        Open Modal
      </button>

      {open && (
        <Modal onClose={() => setOpen(false)} />
      )}
    </>
  );
}

The modal appears in a different DOM location, but it is still part of the React tree.

Events still work through the React tree

One of the most useful things about portals is that moving the DOM location does not move the component out of the React tree.

For example:

function App() {
  return (
    <div onClick={() => console.log("App clicked")}>
      <Modal />
    </div>
  );
}

Even if Modal uses a portal and its DOM is physically rendered somewhere else, React events can still propagate according to the React component tree.

This is an important distinction:

React tree
    App
     │
    Modal

The DOM may look like:

body
├── #root
│   └── App content
│
└── #modal-root
    └── Modal content

The React relationship remains intact even though the DOM locations differ.

Portal does not mean a separate React application

A portal is not another React root.

This:

createPortal(<Modal />, modalRoot)

does not create an independent React application.

It moves the rendered DOM placement while keeping the component connected to the existing React tree.

flushSync

flushSync() is an advanced React DOM API used when you need React to update the DOM synchronously before continuing with some other code.

It is imported from react-dom:

import { flushSync } from "react-dom";

The basic syntax is:

flushSync(() => {
  setSomething(value);
});

Normally, React decides when to process state updates so that it can efficiently render the application.

For example:

setCount(count + 1);
console.log("Update requested");

React controls when the corresponding DOM update is committed.

In special situations, you may need the DOM to be updated immediately before another operation.

That is where flushSync() can help.

Basic example

import { flushSync } from "react-dom";

function handleClick() {
  flushSync(() => {
    setOpen(true);
  });

  // Code after this can observe the DOM
  // after the update has been flushed.
}

The state update inside flushSync() is flushed synchronously.

Why would you need this?

Imagine that a user clicks a button and your application needs to:

  1. Update the React state.
  2. Immediately make a DOM change available.
  3. Measure or interact with that updated DOM.
  4. Continue with another browser operation.

Normally, React may process the update according to its scheduling and batching behavior.

In certain integration scenarios, you may need to force the update to be committed first.

For example:

flushSync(() => {
  setItems(newItems);
});

const height = listRef.current?.scrollHeight;

The intention is that the state update has been flushed before the DOM measurement occurs.

flushSync() should not be used everywhere

flushSync() is an escape hatch, not a normal state-management technique.

Do not turn ordinary state updates into:

flushSync(() => {
  setCount(count + 1);
});

just because you want React to feel faster.

It can reduce React’s ability to batch updates efficiently and may cause additional rendering work.

Use it only when a specific integration or DOM timing requirement makes it necessary.

A common use case

One possible use case is integrating React with code that immediately reads the DOM after a state update.

For example:

function handleAddItem() {
  flushSync(() => {
    setItems(items => [
      ...items,
      "New Item"
    ]);
  });

  const list = listRef.current;

  if (list) {
    list.scrollTop = list.scrollHeight;
  }
}

The important idea is not that flushSync() should be used whenever you manipulate the DOM.

Instead, it is useful when you specifically need React to commit an update before code continues.

React DOM APIs at a Glance

Here is a quick comparison of the APIs covered in this chapter:

APIPurposeTypical use
createRoot()Creates a React rootStarting a client-side application
root.render()Renders React contentDisplaying an application
root.unmount()Removes a React rootDynamically removing a React application
hydrateRoot()Hydrates server-rendered HTMLServer-side rendered applications
createPortal()Renders into another DOM nodeModals, dialogs, tooltips
flushSync()Forces synchronous DOM flushingAdvanced DOM/integration scenarios

createRoot vs hydrateRoot

These two APIs can look similar because both establish a React root, but their purposes are different.

Client-side rendering:

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

root.render(<App />);

Server-rendered HTML:

hydrateRoot(
  document.getElementById("root"),
  <App />
);

A simple way to remember the difference is:

No React HTML exists yet
        ↓
    createRoot()

React HTML already exists
        ↓
    hydrateRoot()

createPortal vs Multiple Roots

Portals and multiple React roots are also different concepts.

A portal:

createPortal(<Modal />, modalRoot);

keeps the component inside the existing React tree.

Multiple roots create separate React trees:

const rootA = createRoot(elementA);
const rootB = createRoot(elementB);

These roots are independent.

Use a portal when you want a component to remain part of the same React application but appear elsewhere in the DOM.

React DOM and Browser DOM

It is useful to understand that React does not replace the browser DOM.

The browser still provides the actual DOM:

HTML
  ↓
Browser DOM
  ↓
React DOM integration
  ↓
React application

React creates and updates DOM elements as part of its rendering process.

For example:

function App() {
  return <button>Click Me</button>;
}

React works with the browser DOM so that the user eventually sees an actual:

<button>Click Me</button>

element on the page.

Common Mistakes

Calling createRoot() repeatedly

Avoid creating multiple roots for the same container.

Instead of repeatedly doing:

createRoot(container).render(<App />);

create the root once:

const root = createRoot(container);

root.render(<App />);

Using createRoot() when hydration is required

If HTML was generated by React on the server and needs to become interactive, use:

hydrateRoot(container, <App />);

rather than treating the existing server-rendered HTML as an empty client root.

Using portals unnecessarily

Portals are useful when the DOM placement needs to be different from the component’s normal location.

Do not use portals simply because they are available.

Using flushSync() for normal state updates

This:

flushSync(() => {
  setCount(count + 1);
});

is generally unnecessary for ordinary interactions.

Normal React state updates should usually be allowed to use React’s normal scheduling and batching.

Forgetting cleanup when unmounting external integrations

If a component subscribes to an external system, make sure its Effect cleanup disconnects or removes the resource.

For example:

useEffect(() => {
  const connection = connect();

  return () => {
    connection.disconnect();
  };
}, []);

This becomes particularly important when React roots are dynamically mounted and unmounted.

Practical React DOM Example

Here is a small example that combines several concepts:

import { useState } from "react";
import { createPortal } from "react-dom";
import { createRoot } from "react-dom/client";

function Modal({ onClose }) {
  return createPortal(
    <div className="modal-backdrop">
      <div className="modal">
        <h2>Welcome</h2>
        <p>This modal is rendered using a portal.</p>

        <button onClick={onClose}>
          Close
        </button>
      </div>
    </div>,
    document.getElementById("modal-root")
  );
}

function App() {
  const [open, setOpen] = useState(false);

  return (
    <main>
      <h1>React DOM Example</h1>

      <button onClick={() => setOpen(true)}>
        Open Modal
      </button>

      {open && (
        <Modal onClose={() => setOpen(false)} />
      )}
    </main>
  );
}

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

root.render(<App />);

The HTML might contain:

<body>
  <div id="root"></div>
  <div id="modal-root"></div>
</body>

The main application is rendered into:

#root

while the modal is rendered into:

#modal-root

Even though they occupy different DOM locations, the modal remains part of the same React tree.

Best Practices

When working with React DOM:

  • Use createRoot() for modern client-side React applications.
  • Create a root once for a given container.
  • Use root.render() to render the React tree.
  • Use root.unmount() when a dynamically mounted React tree is no longer needed.
  • Use hydrateRoot() for server-rendered React HTML that needs to become interactive.
  • Use createPortal() for UI that needs a different DOM location, such as modals and overlays.
  • Keep portal content accessible and manage focus appropriately for dialogs.
  • Use flushSync() only when synchronous DOM commitment is genuinely required.
  • Prefer React’s normal rendering and scheduling behavior for ordinary updates.
  • Clean up subscriptions, timers, connections, and external resources when components unmount.
  • Avoid mixing manual DOM manipulation with React-managed DOM unless there is a specific integration reason.

Summary

React DOM provides the APIs that connect React applications with the browser’s DOM.

createRoot() creates a React root for client-side rendering:

const root = createRoot(container);

root.render() displays React content inside that root:

root.render(<App />);

root.unmount() removes React’s control over the root:

root.unmount();

hydrateRoot() is used when React needs to take over server-rendered HTML:

hydrateRoot(container, <App />);

createPortal() allows a component to render its DOM into another DOM node while remaining part of the same React tree:

createPortal(<Modal />, modalRoot);

Finally, flushSync() is an advanced API that can force React to flush an update synchronously when a particular DOM timing requirement makes that necessary:

flushSync(() => {
  setValue(newValue);
});

Understanding these APIs gives you a solid foundation for understanding how React applications connect components, rendering, server-generated HTML, and the browser’s actual DOM.

Leave a Comment