React Render HTML

Before React can show anything, something has to take your component code and turn it into real HTML elements sitting in the browser’s DOM. That process is called rendering. In React, rendering isn’t something you do by hand with string concatenation or innerHTML — you hand React a component, tell it which DOM node to take over, and React figures out what actual HTML to produce and keeps it updated. This lesson covers exactly how that hand-off works: the API you call, what happens under the hood, and the mistakes beginners hit on day one.

Overview / How it works

A React app starts life as a nearly empty HTML page. Open the page source of any React app and you’ll typically find a single empty container, usually <div id="root"></div>, plus a script tag that loads your JavaScript bundle. Everything visible on the page is generated by React at runtime and inserted into that one div. React never touches anything outside of it.

To hand control of that div to React, you call createRoot(domNode) from react-dom/client. This creates a root, a connection between a real DOM node and a tree of React components. Calling root.render(<App />) on that root tells React: “render the App component tree inside this node, and keep it in sync from now on.”

Here’s what actually happens when render() runs for the first time. Your JSX, like <h1>Hello</h1>, is not HTML at all — it’s syntax that a build tool (Babel, via Vite or your bundler) compiles into plain JavaScript function calls that produce lightweight JavaScript objects describing what should appear on screen. This object tree is often called the virtual DOM. React walks that tree, figures out what real DOM nodes are needed (an h1 element with a text node inside it, for example), creates them, and inserts them into your root container. This first pass is called mounting.

Later, when state changes (covered in the state and hooks lessons), React does not tear everything down and rebuild it from scratch. Instead it re-runs your component function to get a new virtual DOM tree, compares it to the previous one in a process called reconciliation, and only touches the real DOM nodes that actually changed. This is why React is fast, and why you should think of your component as a description of “what the UI should look like right now” rather than a series of manual DOM edits. Rendering, in the React sense, always means: run the component function, get back a description of the UI, and let React translate that description into real elements.

Syntax

The general form for rendering a React app into a page looks like this:

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

const root = createRoot(document.getElementById("root"));
root.render(<App />);
Part What it does
createRoot(domNode) Creates a React root attached to a specific real DOM element. Called once per app.
document.getElementById("root") Finds the empty container element in your HTML page that React will take ownership of.
root.render(<App />) Tells the root which component tree to display. React mounts it on the first call and updates it on later calls or state changes.
<App /> JSX for your top-level component. Everything your app shows is nested inside this one component tree.

Note that ReactDOM.render(<App />, container), the older API, is deprecated as of React 18. Modern React always uses createRoot followed by root.render.

Examples

Example 1: Rendering a simple heading

A typical project has an HTML shell with an empty root div, and a JavaScript entry file that renders into it:

<!doctype html>
<html>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
import { createRoot } from "react-dom/client";

function App() {
  return <h1>Hello, World!</h1>;
}

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

Output:

Hello, World!

The App function returns a single JSX element. React compiles that JSX, mounts it into the empty #root div, and the browser displays it as a plain <h1> tag — even though you never wrote HTML directly.

Example 2: Rendering with embedded JavaScript expressions

Anything inside curly braces in JSX is evaluated as a regular JavaScript expression and rendered as text:

import { createRoot } from "react-dom/client";

function App() {
  const name = "Ava";
  const birthYear = 2001;
  const age = 2026 - birthYear;

  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>You are {age} years old.</p>
    </div>
  );
}

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

Output:

Hello, Ava!
You are 25 years old.

Notice that {name} and {age} are not strings — they’re JavaScript variables computed above the return statement. React renders whatever value the expression evaluates to (a number, a string, the result of a function call) as text inside the surrounding markup.

Example 3: Rendering multiple sibling elements with a Fragment

A component can only return one root element, so to render several top-level siblings without adding an extra wrapper <div> to the real DOM, use a Fragment (<>...</>):

import { createRoot } from "react-dom/client";

function App() {
  return (
    <>
      <h1>Welcome to React</h1>
      <p>This paragraph and the heading above are siblings.</p>
      <p>They're grouped with a Fragment instead of an extra div.</p>
    </>
  );
}

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

Output:

Welcome to React
This paragraph and the heading above are siblings.
They're grouped with a Fragment instead of an extra div.

The heading and two paragraphs all appear as direct children of #root in the real DOM — the Fragment itself produces no actual HTML element, it just satisfies JSX’s single-root-element rule.

How it works step by step / Under the hood

  • On mount: root.render(<App />) runs the App function, which returns a tree of JSX (compiled to React.createElement calls under the hood). React walks that tree, creates matching real DOM nodes, and inserts them all into the root container in a single batch, called the commit phase.
  • On update: Something (state, a prop change) causes a component to re-run. React builds a new virtual DOM tree, diffs it against the previous one, and patches only the real DOM nodes that differ — it does not remove and rebuild the whole tree. This diffing/patching process is called reconciliation, and it’s covered in depth in the Virtual DOM lesson.
  • On unmount: If a parent stops rendering a component (for example, it’s removed from a list or hidden behind a condition), React removes its DOM nodes and runs any cleanup registered in useEffect cleanup functions.

Common Mistakes

Mistake 1: Returning adjacent elements without a wrapper

JSX requires a single root element. Returning two siblings directly is a build error:

function App() {
  return (
    <h1>Welcome</h1>
    <p>This will not compile.</p>
  );
}

This fails because JSX can’t return two elements side by side from one function. Wrap them in a Fragment (or a single element) instead:

function App() {
  return (
    <>
      <h1>Welcome</h1>
      <p>This compiles correctly.</p>
    </>
  );
}

Mistake 2: Using class instead of className

JSX attributes map to DOM properties, and class is a reserved JavaScript keyword, so React uses className instead:

function App() {
  return <div class="card">Profile</div>;
}

React will render this, but it logs a warning and ignores the attribute as real CSS wiring in some tooling setups. The correct version is:

function App() {
  return <div className="card">Profile</div>;
}

Best Practices

  • Call createRoot exactly once per app, at your entry file (commonly main.jsx or index.jsx).
  • Keep the HTML root element empty and let React own everything inside it — don’t manually add or edit HTML inside that container.
  • Prefer Fragments (<>...</>) over unnecessary wrapper <div> elements when a component needs to return multiple siblings.
  • Always use the JSX-specific attribute names: className instead of class, htmlFor instead of for, camelCase event handlers like onClick.
  • Let React re-render your UI in response to state changes rather than calling root.render() again yourself — that call is only for the initial mount.

Practice Exercises

  • Create an App component that renders your name in an <h2> and a short sentence about your favorite hobby in a <p>, then mount it with createRoot and root.render.
  • Take the broken “adjacent elements” example from Common Mistakes and fix it using a Fragment so it renders both lines correctly.
  • Build a small profile component that computes a person’s age from a stored birth year using a JavaScript expression inside {}, and renders both their name and computed age.

Summary

  • Rendering means turning your component tree into real DOM elements inside one container node.
  • createRoot(domNode) creates a root tied to a real DOM element; root.render(<App />) mounts your component tree into it.
  • JSX compiles to plain JavaScript objects (the virtual DOM) before React turns it into real HTML elements.
  • The first render is called mounting; later updates use reconciliation to patch only what changed.
  • A component must return a single root element — use Fragments to group siblings without adding extra DOM nodes.
  • Use className, not class, and other JSX-specific attribute names.