mrkeyoor.com_
Wed 05 Aug 23:09 UTC
npmWeb Frontendupdated 05 Aug 2026

react-dom

react-dom is React's renderer for the browser DOM: createRoot and hydrateRoot mount component trees, plus portals, flushSync, form-status hooks, resource preloading, and the server rendering entry points under react-dom/server and react-dom/static. React itself (the react package) is renderer-agnostic; react-dom is the half that actually touches document. The 1.4KB bundlephobia number is only the package root shim; the real renderer you ship comes in through react-dom/client and is an order of magnitude larger.

Verdict

The default web renderer for the dominant UI library; if you chose React you have chosen react-dom, and it is excellent at its job. The honest caveat is that you rarely interact with it directly anymore beyond createRoot, portals, and the form hooks, and if bundle size matters more than ecosystem, preact covers most apps.

API stability4/5createRoot has been the stable mount API since React 18 (2022), and React 19's removals (render, findDOMNode) were deprecated for years first; still, a major that deletes APIs costs real migration work in old codebases.
Docs5/5react.dev documents every react-dom export with live examples, migration guides, and honest pitfall sections; it is one of the best-documented packages on npm.
Maintenance5/5Developed daily in the react/react monorepo (247k stars, pushed today) by a funded full-time team; 19.2.8 shipped July 2026 with steady patch releases.
Ecosystem5/5154M weekly downloads and the largest component ecosystem in frontend; nearly every UI library, framework, and job posting assumes it.

Use it if

  • You are building for the browser with React: this is not optional, react without react-dom renders nothing on the web
  • You need an escape hatch from a framework: portals for modals and tooltips, flushSync for the rare must-be-synchronous DOM read after an update
  • You do custom SSR without a framework and need renderToPipeableStream or the static prerender APIs directly
  • You want React 19 form features: useFormStatus and form actions live in react-dom, not react
Skip it if

Setup reality

npm install react react-dom, and keep the two versions exactly in sync: a mismatch (or two copies from a hoisting accident) produces the famously unhelpful 'invalid hook call' error. React 19 removed the long-deprecated APIs, so older codebases must move to createRoot before upgrading, and libraries that still call findDOMNode simply break. You also need a build step for JSX, but in 2026 every bundler template handles that; the real setup decisions have moved up a level into frameworks like Next.js and React Router, where you rarely import react-dom yourself except for portals.

Patterns

Mount an app with createRootmount-app

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

const root = createRoot(document.getElementById('root'))
root.render(<App />)

The only mount API since React 19: ReactDOM.render was removed after being deprecated through 18. Note the import is 'react-dom/client', not 'react-dom'.

Hydrate server-rendered HTMLhydrate-ssr

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

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

The client tree must render the same output as the server HTML or React logs hydration mismatches and re-renders client-side; anything time- or random-dependent needs useEffect or suppressHydrationWarning.

Render a modal through a portalportal-modal

import { createPortal } from 'react-dom'

function Modal({ children }) {
  return createPortal(
    <div className="overlay">{children}</div>,
    document.body,
  )
}

The DOM node moves but the React tree does not: context, state, and event bubbling still follow the component hierarchy, which is exactly what you want for modals inside providers.

Force a synchronous DOM updateflush-sync

import { flushSync } from 'react-dom'

flushSync(() => {
  setItems((prev) => [...prev, newItem])
})
// DOM is updated here, safe to measure
listRef.current.lastElementChild.scrollIntoView()

Opts out of React 18+ batching so you can read layout right after a state change. It hurts performance and the docs call it a last resort; scrolling-to-new-content is the one legitimate everyday use.

Pending state with useFormStatusform-status

import { useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending } = useFormStatus()
  return <button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>
}

// <form action={saveAction}><SubmitButton /></form>

React 19 API. It only reports the status of a parent form element, so the hook must be called from a component rendered inside the form, not from the component that renders the form itself.

Preload resources imperativelypreload-resources

import { preload, preconnect, preinit } from 'react-dom'

function Card() {
  preconnect('https://cdn.example.com')
  preload('/fonts/inter.woff2', { as: 'font', crossOrigin: 'anonymous' })
  preinit('/analytics.js', { as: 'script' })
  return <article>...</article>
}

React 19 resource APIs: calls are deduplicated and during SSR they emit <link> tags in the head. preload hints a fetch, preinit actually executes/inserts the resource.

Stream SSR with renderToPipeableStreamssr-streaming

import { renderToPipeableStream } from 'react-dom/server'

app.get('/', (req, res) => {
  const { pipe } = renderToPipeableStream(<App />, {
    bootstrapScripts: ['/main.js'],
    onShellReady() {
      res.setHeader('content-type', 'text/html')
      pipe(res)
    },
  })
})

Streams the shell immediately and flushes Suspense boundaries as they resolve. This is the Node entry point; edge runtimes use renderToReadableStream from the same package.

Prerender fully static HTMLstatic-prerender

import { prerenderToNodeStream } from 'react-dom/static'

const { prelude } = await prerenderToNodeStream(<App />, {
  bootstrapScripts: ['/main.js'],
})
prelude.pipe(fileStream)

Unlike renderToPipeableStream, the static APIs wait for all data and Suspense to resolve and produce complete HTML, which is what you want for build-time static generation.

Catch rendering errors at the rootroot-error-handlers

const root = createRoot(container, {
  onUncaughtError: (error, info) => {
    reportError(error, info.componentStack)
  },
  onCaughtError: (error) => {
    // errors already handled by an ErrorBoundary
  },
})

React 19 root options that replace patching console.error for error reporting; onRecoverableError also exists for hydration-mismatch recovery.

Migrate removed React 18-era APIslegacy-migration

// removed in React 19:
// ReactDOM.render(<App />, el)
// ReactDOM.unmountComponentAtNode(el)

import { createRoot } from 'react-dom/client'
const root = createRoot(el)
root.render(<App />)
root.unmount()  // replaces unmountComponentAtNode

findDOMNode is also gone with no direct replacement: use a ref on the element instead. Libraries last published before 2023 are the usual source of these crashes after upgrading.

Keep react and react-dom in lockstepversion-lock

{
  "dependencies": {
    "react": "19.2.8",
    "react-dom": "19.2.8"
  }
}
// debugging a mismatch:
// npm ls react react-dom

Version skew or duplicated copies cause 'invalid hook call' and 'cannot read properties of null' errors deep in hooks. npm ls showing two react versions is the diagnosis 90% of the time.

Alternatives

PackageRegistryPick it when
preactnpmYou want the React programming model at a fraction of the bundle size and can live with compat-layer edge cases.
solid-jsnpmYou want JSX with fine-grained reactivity and no virtual DOM, trading the React ecosystem for faster updates.
sveltenpmYou prefer compiling components to minimal imperative DOM code over shipping a runtime renderer at all.