ReactRendering Elements to the DOM

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:

HTML
<!-- 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:

TSX
// 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:

TSX
// 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>
)
Note
The new `createRoot` API unlocks React 18's concurrent features: automatic batching, `useTransition`, `useDeferredValue`, and Suspense improvements. The legacy `ReactDOM.render` runs in a legacy mode that disables those features even in React 18.
What Happens During Initial Render

When root.render() is called, React performs the following steps:

  • Element creation — React calls your App component function, which returns a JSX element tree

  • Reconciliation — 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 useEffect callbacks

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:

TSX
// 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:

TSX
// 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

TSX
// 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>
)
Tip
The non-null assertion `!` in `document.getElementById('root')!` tells TypeScript that the element definitely exists. It's safer to add an explicit null check and throw a meaningful error, as shown above — that way you get a clear message if `index.html` is misconfigured rather than a cryptic "Cannot read properties of null" error deep inside React internals.
Warning
Never put the `<script>` tag loading your JS bundle before the `<div id="root">` in your HTML without `defer` or `type="module"`. If the script runs before the DOM is parsed, `document.getElementById('root')` returns `null` and the app silently fails to mount.
Summary
  • React mounts into a single root DOM node: <div id="root"> in index.html

  • ReactDOM.createRoot(element) creates a React 18 concurrent root

  • root.render(<App />) kicks off the initial render — you only call this once

  • After mount, React updates the DOM automatically when state/props change

  • React 17's ReactDOM.render() is deprecated — use createRoot in new projects

  • Multiple independent roots are supported on one page

  • Always wrap your app in <React.StrictMode> during development