Rendering Elements to the DOM
Every React application begins with a single JavaScript call that mounts the component tree into a real DOM node. Understanding this bootstrap sequence — the entry point, the root node, and the render call — gives you a clear mental model of how React takes over the browser page.
The Root DOM Node
A React app is mounted inside a single HTML element, conventionally a <div> with id="root". This element is the only piece of HTML you write by hand in your index.html. Everything else is rendered by React:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My React App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>The <div id="root"> starts empty. React will populate it entirely during the initial render. Nothing inside that div should be written by hand — React owns it.
The Entry Point: main.tsx / main.jsx
The entry file (commonly src/main.tsx in a Vite project or src/index.tsx in Create React App) selects the root DOM node and tells React to render your top-level component into it. In React 18 this looks like:
// src/main.tsx (React 18)
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
const rootElement = document.getElementById('root')!
const root = ReactDOM.createRoot(rootElement)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)React 17 vs React 18 Render API
React 18 introduced a new root API (createRoot). The old API (ReactDOM.render) still works in React 18 but is deprecated and will be removed in a future version. Here is the comparison:
// React 17 — legacy render (deprecated in React 18)
import ReactDOM from 'react-dom'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
// React 18 — new concurrent root API
import ReactDOM from 'react-dom/client'
const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)What Happens During Initial Render
When root.render() is called, React performs the following steps:
Element creation — React calls your
Appcomponent function, which returns a JSX element treeReconciliation — React walks the element tree and creates a corresponding "fiber" tree in memory (the Virtual DOM)
Commit — React walks the fiber tree and creates real DOM nodes for each element, then inserts them into
<div id="root">Effects — After the DOM is updated, React runs any
useEffectcallbacks
The browser paints the screen after React has committed its changes to the real DOM. From the user's perspective, the page goes from blank to fully rendered in one frame.
Updating the UI
After the initial render, React updates the DOM only when state or props change. You do not call root.render() again — React handles updates internally through hooks like useState and useReducer. When state changes, React re-renders the affected components and diffs the new element tree against the previous one, applying only the minimal set of DOM changes needed:
// React efficiently updates only what changed
function Clock() {
const [time, setTime] = React.useState(new Date())
React.useEffect(() => {
const id = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(id)
}, [])
return (
<div>
<h1>Current time</h1> {/* Never re-rendered — didn't change */}
<p>{time.toLocaleTimeString()}</p> {/* Re-rendered every second */}
</div>
)
}Even though the Clock component re-renders every second, React only updates the text node inside <p>. The <h1> element is untouched.
Multiple React Roots
A single page can have multiple independent React roots. This is common when gradually migrating a server-rendered app to React, or embedding React widgets into a CMS:
// Mount two independent React trees on the same page
const headerRoot = ReactDOM.createRoot(document.getElementById('react-header')!)
headerRoot.render(<Header />)
const widgetRoot = ReactDOM.createRoot(document.getElementById('react-widget')!)
widgetRoot.render(<Widget />)
// Both trees are completely independent — state in one does not
// affect the other unless you explicitly share it (e.g. via context
// or a store that both subscribe to).React.StrictMode
The entry point almost always wraps the app in <React.StrictMode>. StrictMode is a development-only tool that activates extra checks and warnings. It has no effect on the production build. It is covered in depth on the next page, but the key point is: always wrap your app in StrictMode during development.
Complete Vite Entry File
// src/main.tsx — complete entry file for a Vite + React 18 project
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import './index.css'
const rootEl = document.getElementById('root')
if (!rootEl) {
throw new Error(
'Root element #root not found. Make sure index.html contains <div id="root">.'
)
}
ReactDOM.createRoot(rootEl).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)Summary
React mounts into a single root DOM node:
<div id="root">inindex.htmlReactDOM.createRoot(element)creates a React 18 concurrent rootroot.render(<App />)kicks off the initial render — you only call this onceAfter mount, React updates the DOM automatically when state/props change
React 17's
ReactDOM.render()is deprecated — usecreateRootin new projectsMultiple independent roots are supported on one page
Always wrap your app in
<React.StrictMode>during development