In React 19, what is the difference between calling hydrateRoot(container, <App />) and createRoot(container).render(<App />), and what goes wrong if you use createRoot on a container that already holds server-rendered HTML?
answer
- one container is full, the other empty
- where to render versus what to render
- hydration only ever applies to the first render
- the wrong choice fails silently
- legacy ReactDOM.hydrate is gone in 19
basics
~10 screateRoot renders a fresh tree into a container; hydrateRoot adopts server-rendered HTML already inside it, reusing those nodes. Calling createRoot on server HTML discards that markup and re-renders from scratch, wasting the server work.
solid answer
~50 sBoth come from `react-dom/client` and both give you a root object, but they start from opposite assumptions. `createRoot(container)` assumes the container is yours to fill: you then call `root.render(<App />)` and React creates every DOM node. `hydrateRoot(container, <App />)` takes the tree as its second argument and assumes the container already contains matching server HTML, so React renders the tree in memory and adopts the existing nodes instead of creating them. If you point `createRoot` at server-rendered markup, React does not attempt hydration at all — there is nothing to mismatch, because it never compares. It simply replaces the container's contents with a fresh client render. Functionally the page works, which is what makes the bug easy to miss; what you lose is the entire benefit of server rendering: the server's HTML is thrown away, the browser re-paints, and your paint metrics regress. In React 19 the legacy `ReactDOM.hydrate` is gone, so `hydrateRoot` is the API.
code
javascript · 17 linesimport { createRoot, hydrateRoot } from 'react-dom/client';
function App() {
return <h1>Hello</h1>;
}
const ssrContainer = document.getElementById('root');
const widgetContainer = document.getElementById('widget');
// Container already holds markup from the server: adopt it.
const appRoot = hydrateRoot(ssrContainer, <App />);
// Later updates through the same root are ordinary client renders.
appRoot.render(<App />);
// A separate, empty container gets a plain client root.
createRoot(widgetContainer).render(<App />);go deeper
Remember that server-rendered pages use hydrateRoot and client-only pages use createRoot, and that hydrateRoot takes the container and the tree together in one call.
Explain why the signatures differ — hydration only applies to the first render — and describe what React does to a container it was told is empty.
Emphasise that the wrong API fails silently: correct output, discarded server work, regressed paint metrics. Say how you would catch it in review or in monitoring.
Frame the root API as the opt-in to the concurrent renderer, and be ready to say what a migration off the removed legacy entry points costs across a large codebase.
## Two roots, two starting assumptions React 19 has exactly two ways to create a root in the browser, both exported from `react-dom/client`: ```javascript import { createRoot, hydrateRoot } from 'react-dom/client'; // Container is empty; React builds all of the DOM. const root = createRoot(document.getElementById('root')); root.render(<App />); // Container already holds server HTML; React adopts it. const hydrated = hydrateRoot(document.getElementById('root'), <App />); ``` The difference is not cosmetic. `createRoot` is told *where* to render and then, separately, *what* to render. `hydrateRoot` needs both at once, because the very first render is the one that has to line up with the markup already sitting in the container — there is no such thing as hydrating later. ## The signature difference is a consequence, not a quirk Candidates often mis-remember the shape and write `hydrateRoot(container).render(<App />)`. That does not exist, and understanding why is the interesting part: hydration is a property of the *initial* render. React must know the tree at the moment it starts walking the server's DOM. Once that first pass is done, the root behaves like any other root — calling `hydratedRoot.render(nextTree)` performs a normal client update, not a second hydration. ## What actually happens with the wrong choice Suppose your server streams a complete page and your client entry calls `createRoot(container).render(<App />)`. React has been told the container is a blank canvas, so it never inspects the existing children and never compares anything. It renders the tree from scratch, creating every DOM node, and replaces the container's contents. The consequences are all performance and perception, not correctness: - **The server's work is discarded.** You paid for server rendering and then threw the result away. - **The browser re-does layout and paint** for content it had already painted, which can show as a visible flash or a content shift. - **Anything the browser had attached to the old nodes is gone** — focus, in-progress text selection, media element state — because those nodes no longer exist. - **No hydration error is reported**, because no hydration was attempted. This is why the bug survives code review: nothing in the console complains. The inverse mistake — `hydrateRoot` on an empty container — is loud rather than silent: React finds no markup to adopt and reports a hydration error, then renders on the client. ## React 19 removals worth naming React 19 removed the legacy `ReactDOM.render` and `ReactDOM.hydrate` entry points along with `ReactDOM.unmountComponentAtNode`. The root APIs are not a stylistic preference; they are the only way to opt into the concurrent renderer, which is what makes streaming and selective hydration possible. If an interviewer shows you `ReactDOM.hydrate(<App />, container)`, the correct answer is that it is React 17-era code that no longer runs on 19, and the migration is a one-line change to `hydrateRoot`. ## More than one root on a page The two APIs coexist happily in one document. A page can hydrate a server-rendered container with `hydrateRoot` while a separate widget mounted into an empty `<div>` uses `createRoot`. Each root is independent: its own tree, its own listeners on its own container, its own updates. That is the mechanism behind embedding React into a page that is mostly rendered by something else, and it is also how you keep a genuinely client-only surface out of the hydration path. ## Interview framing Say the assumption each API makes, then the failure mode. The strongest version of the answer notes that the wrong choice is *silent* — the page still works, which is exactly why it needs to be caught by knowing the APIs rather than by watching for errors.
- After hydrateRoot returns a root, what does calling root.render() on that root do?An ordinary client update. Hydration is a property of the first render only — once the server markup has been adopted, the root behaves exactly like one created with `createRoot`, diffing the new tree against the current one and applying changes. There is no way to hydrate a second time through the same root.
- Can one page hydrate part of the document and client-render another part?Yes. Roots are independent, so you can call `hydrateRoot` on a server-rendered container and `createRoot` on a separate empty container in the same document. Each root has its own tree and its own listeners on its own container, which is how React embeds into pages that are mostly rendered by something else.
- What happens if you call hydrateRoot on a container that turns out to be empty?React finds nothing to adopt, reports a hydration error, and falls back to rendering the tree on the client. Unlike the reverse mistake it is noisy, so it usually surfaces immediately in development — the classic cause is a client entry point running against a page that was not actually server-rendered.
saying these in an interview costs you the question
- Writes hydrateRoot(container).render(app) as if it chained
- Thinks createRoot on server HTML logs a hydration mismatch
- Believes createRoot appends beside the existing server markup
- Says ReactDOM.hydrate is still the React 19 API
- Assumes every root.render call after hydration re-hydrates