createRoot and the Root Container
main.jsx is probably one of the smallest files in your React project, but this tiny file is what starts the whole app in the browser.
And when you're new to React, it can look a bit weird because you have index.html, then some div with id="root", then document.getElementById(), then createRoot(), then root.render(), and somehow after all of that your App component appears on screen.
So what exactly happened there?
A browser React app needs an existing DOM element first. React DOM takes that DOM element and creates a React root for it, and then you give that root some React content to render.
There are three different things there: the DOM container, the React root object, and your React component tree. Try not mixing these together because they aren't the same thing and you'll be dealing with all three of them quite a lot.
index.html creates the DOM container
main.jsx finds that container
createRoot creates a React root for it
root.render gives that root some React contentLet's go through what all of this is actually doing.
First, where does the container come from?
Open index.html from your project root. You'll probably find something similar to this inside <body>:
<div id="root"></div>React didn't create this div.
The browser did.
The browser parses your HTML document and creates an actual HTMLDivElement in the DOM for this tag. The id="root" is just an HTML ID sitting on that element.
And btw, there is nothing magical about the word root.
You could write:
<main id="application"></main>and React would be completely fine with it. You can call the ID application, reactbook, app, whatever. React only needs a DOM element to attach to.
The reason everybody uses root is mostly convention, and because templates usually already give you that name.
The important part is that your JavaScript has to look for the same ID that exists in HTML.
Your index.html also normally has something like this:
<script type="module" src="/src/main.jsx"></script>This tells the browser to load your main.jsx module.
Since this is a module script, the browser parses the document before executing it, so that earlier <div id="root"> already exists by the time your startup code runs.
Now open main.jsx.
A Vite React project usually starts with imports similar to these:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./index.css";StrictMode comes from React and enables extra development checks. createRoot comes from React DOM and is what we'll use to connect React to the browser DOM. App is our own component. And index.css loads the CSS for the page.
Then comes this line:
const container = document.getElementById("root");Remember from the previous chapter, document belongs to the browser. React has nothing to do with document.getElementById().
We're literally asking the browser, "give me the element whose ID is root."
If the browser finds it, container now holds that DOM element.
Then:
const root = createRoot(container);This is the point where React DOM comes in.
We give createRoot() the browser DOM element, and React DOM gives us back a React root object.
Then finally:
root.render(<App />);Now we're telling that React root what React content it should render.
So container and root are two completely different values.
container is an actual browser DOM element. root is an object returned by React DOM.
That difference sounds obvious once you say it out loud, but it's very easy to mentally merge both when you're first looking at this code.
What if the container doesn't exist?
There is one small problem with this:
const container = document.getElementById("root");getElementById() can return null.
Maybe somebody changed the HTML:
<div id="application"></div>but forgot to change this:
const container = document.getElementById("root");Now there is no element called root in the document, so container becomes null.
And createRoot() needs a DOM element. Giving it null isn't going to start your app.
Because of that, I prefer checking the container before doing anything else:
const container = document.getElementById("root");
if (!container) {
throw new Error('Missing the DOM element with id="root"');
}Then create the root only after that check:
const root = createRoot(container);
root.render(<App />);This is also just nicer when something breaks.
If your error only says:
Failed to startwell... okay. Very useful.
What failed? Why did it fail? Which value was missing?
But this:
Missing the DOM element with id="root"tells you exactly what happened.
And if you're using TypeScript, the same check helps there too. Before the if, TypeScript sees container as something which could be an HTMLElement or null. After the check, it knows container exists.
So what is createRoot() actually creating?
Let's look at this again:
const root = createRoot(container);A React root is not another DOM element.
You can't do browser DOM stuff on it:
root.appendChild(node);That doesn't work because root isn't an HTMLElement.
The real DOM element is still sitting in container.
What the root gives you are React DOM methods, mainly things such as:
root.render(...)
root.unmount()render() tells React what should be rendered inside this root. unmount() removes the React tree from the container and cleans it up.
Also, create the root once.
Don't do this:
const firstRoot = createRoot(container);
const secondRoot = createRoot(container);React will complain because you've already created a root for that container.
If your app updates later, you don't call createRoot() again.
You keep using the same root.
Actually, for normal app updates, you usually don't even call root.render() again yourself. Components update through state, props, context, router changes, and so on. React handles those renders while the same root stays there.
So this:
const root = createRoot(container);
root.render(<App />);is usually startup code.
You run it when the app begins, and after that your component tree handles normal UI updates.
What does root.render(<App />) do?
Now we get to this line:
root.render(<App />);<App /> creates a React element describing your App component.
React then renders the component tree starting from App, and React DOM updates the DOM inside the root container based on what those components return.
Suppose your HTML started with this:
<div id="root"></div>and your component returns:
function App() {
return <h1>My first React screen</h1>;
}After React renders, the browser DOM ends up with something similar to:
<div id="root">
<h1>My first React screen</h1>
</div>The outer div was already there.
React didn't create that root container.
React manages what gets rendered inside it.
Now what if the container already had some HTML?
<div id="root">
<p>Loading application...</p>
</div>That paragraph can appear before the JavaScript app has loaded, which can sometimes be useful.
But when you call:
root.render(<App />);React takes over the contents of that container. That existing paragraph isn't automatically kept around. Once the first React render completes, the children inside the container should match what your React tree rendered.
There's another React API called hydrateRoot() for pages where React HTML was already rendered on the server and you want React to attach to that existing output instead of replacing it.
That's a different setup though.
If you're making the normal client-side Vite app we've been working with, createRoot() is the API you're interested in.
And what is StrictMode doing?
The Vite starter normally gives you this:
root.render(
<StrictMode>
<App />
</StrictMode>,
);And sooner or later you're going to put a console.log() inside a component, see it print twice in development, and wonder what on earth React is doing.
Very common.
StrictMode enables extra checks while you're developing. React can run some component rendering work again and also repeat some Effect setup and cleanup work so it can catch code which behaves badly when React repeats something.
For example, doing side effects directly while rendering a component can become much easier to notice with these checks enabled.
StrictMode itself doesn't create some <StrictMode> tag in your DOM.
If App returns:
<h1>Hello</h1>the browser sees the h1. There is no StrictMode HTML element around it.
And those extra development checks don't run the same way in your production build.
So if you're seeing some logs twice while developing, don't immediately remove StrictMode just to make the second log disappear. First check why that code behaves differently when React runs it again.
Quite often React is showing you a bug you already had.
How much of the page does the root control?
Only the content belonging to that root.
Suppose your document looks like this:
<body>
<header>ReactBook</header>
<div id="root"></div>
<footer>Copyright 2026</footer>
</body>Then you create a root on that middle div:
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);React doesn't suddenly remove your header and footer.
They're outside the root container.
React is managing what gets rendered inside div#root.
This is why React can also be added to only one part of an existing page. You don't necessarily need React controlling every section of the document.
Maybe your existing site has normal server-rendered HTML everywhere, but you want React only for these two areas:
<div id="reaction-controls"></div>
<div id="reading-progress"></div>You can create a separate root for each one:
const reactionsRoot = createRoot(reactionsContainer);
const progressRoot = createRoot(progressContainer);and render different React components:
reactionsRoot.render(<ReactionControls />);
progressRoot.render(<ReadingProgress />);This works.
But these are two separate React trees.
Just because both containers happen to sit next to each other in the HTML doesn't create any React connection between them. Context from one root doesn't automatically become available inside another root, and state in one tree doesn't somehow become part of the other one.
For a normal full React application, you'll usually have one root and put the whole app below it. Then your components can all belong to the same tree and use the same providers, router, context, error handling, etc.
Multiple roots are more useful when you're adding React into separate parts of some existing page.
Don't let other scripts randomly change React's DOM
Once React is managing the children inside a root, you shouldn't have some other script modifying those same children behind React's back.
For example, suppose React rendered:
<div id="root">
<h1>ReactBook</h1>
</div>and then some random script does:
document.getElementById("root").innerHTML = "";The browser DOM has now changed without React doing it.
React still has its own information about what it rendered there, but another script has removed those nodes directly.
You can get some very confusing behaviour from this.
So if React owns a section of the DOM, let React manage that section.
Normal browser APIs are still completely fine outside that managed content, and sometimes you'll also use DOM APIs through refs when you genuinely need direct access to an element. But don't have unrelated scripts replacing React-managed children whenever they feel like it.
Can you call root.render() again?
Yep.
You could technically do:
root.render(<App mode="reading" />);and later:
root.render(<App mode="review" />);It's still the same root.
React receives the new element tree, renders it, and updates whatever needs changing.
If the component tree remains compatible, React can also preserve existing component state.
But again, in a normal React app you probably aren't calling root.render() whenever the user clicks something.
You'd normally have state somewhere:
const [mode, setMode] = useState("reading");and then:
<App mode={mode} />Changing the state causes React to render again without you manually going back into main.jsx.
The root just stays there.
Now suppose you replace the top component itself:
root.render(<AdminApp />);instead of:
root.render(<App />);That's a much bigger change because React is now receiving a different component type at the top.
The old App component can be removed completely along with its state, and React can create a fresh AdminApp tree.
So React doesn't preserve component state just because two things happened to use the same root container. Component identity in the React tree also decides whether state can stay.
What about root.unmount()?
Sometimes a React section needs to disappear completely while the rest of the page continues running.
Maybe React was added into one part of a larger non-React website, and that section is about to be removed.
You can clean up the React root with:
root.unmount();React removes the rendered content and cleans up the tree, including Effect cleanup functions and subscriptions owned by those components.
After you unmount a root, that root object is finished.
So this doesn't work:
root.unmount();
root.render(<App />);You can't start rendering through the same root again after unmounting it.
If you later need React on that container again, create another root.
Also, don't call unmount() just because the user changed pages inside your React app.
If you're using client-side routing, the React root normally stays mounted while the router changes which components are being shown.
main.jsx should probably stay boring
After understanding all of this, your client entry can still be very small:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
const container = document.getElementById("root");
if (!container) {
throw new Error('Missing the DOM element with id="root"');
}
const root = createRoot(container);
root.render(
<StrictMode>
<App />
</StrictMode>,
);That's really enough for most projects.
You can also write the last part inline:
createRoot(container).render(
<StrictMode>
<App />
</StrictMode>,
);Nothing wrong with that either.
I usually like keeping root in a variable while learning because you can clearly see the two values:
const container = ...
const root = ...One is the DOM element.
One is the React root.
Once you're comfortable with that, using createRoot(container).render(...) directly is completely fine.
But try not turning main.jsx into the file where you start putting random application code just because this is the first file that runs.
Your actual UI belongs inside App and the components below it.
main.jsx should mostly start the app, create the root, load global setup if you have any, and get out of the way.
Check it in the browser once
Open DevTools and look at the Elements panel.
If your App currently returns:
function App() {
return <h1>My first React screen</h1>;
}you should see something similar to:
div#root
h1
My first React screenThe div#root came from index.html.
The h1 came from your React tree.
If you also have React DevTools installed, its Components panel will show your React components, such as App.
These two DevTools panels are showing different things. The Elements panel shows the browser DOM. React DevTools shows the React component tree.
Try breaking the startup once as well.
Change:
<div id="root"></div>to:
<div id="application"></div>but leave this unchanged:
document.getElementById("root");Reload the page.
You should get your own missing-container error because JavaScript asked the browser for an element which doesn't exist.
Then change the HTML back and the app should start again.
This also proves something useful: React doesn't somehow know which element you meant because it happens to be called a React project. It gets the exact DOM element you pass into createRoot().
So if you remember only the startup flow, remember this:
index.html gives the browser a container
document.getElementById finds that DOM element
createRoot(container) creates a React root for it
root.render(<App />) gives React the component tree to renderAfter that, the root usually stays alive for the whole lifetime of your app while React handles updates inside it.