Skip to content
React rc jsx 4 min read

JSX Explained

JSX is the syntax extension that lets you write markup that looks like HTML directly inside JavaScript. It is not a separate template language and it is not actually HTML — it is syntactic sugar that a build tool rewrites into plain function calls before the code ever reaches the browser. Understanding what JSX compiles to, and the handful of rules it enforces, removes most of the early confusion around React.

What JSX is

JSX stands for JavaScript XML. Every tag you write is shorthand for a call to a function that creates a React element — a lightweight JavaScript object describing what should appear on screen. React reads those objects to build and update the DOM.

Crucially, JSX is an expression. It evaluates to a value, so you can assign it to a variable, return it from a function, or pass it as an argument:

const greeting = <h1 className="title">Hello, world</h1>;

function Welcome() {
  return greeting;
}

Because the markup lives inside JavaScript, you get the full power of the language — variables, conditionals, loops, and function calls — right next to your UI, with editor autocomplete and type checking along for the ride.

Why React uses JSX

React’s core idea is that rendering logic is inherently coupled to UI markup: which element shows depends on state, event handlers attach to specific nodes, and data flows into the tree as props. Rather than split that logic across separate template files, React keeps markup and behavior together in components. JSX makes that colocation readable.

You can write React without JSX by calling React.createElement yourself, but it quickly becomes unwieldy. JSX gives you a declarative, HTML-like view of the same tree.

How JSX compiles to React.createElement

A JSX tag is a more readable way to write a createElement call. The signature is React.createElement(type, props, ...children).

// What you write:
const el = <h1 className="title">Hello</h1>;

// What the compiler emits (classic runtime):
const el = React.createElement("h1", { className: "title" }, "Hello");

Nesting maps to nested calls. Children become trailing arguments:

// What you write:
const card = (
  <div className="card">
    <h2>Ada Lovelace</h2>
    <p>First programmer</p>
  </div>
);

// What the compiler emits:
const card = React.createElement(
  "div",
  { className: "card" },
  React.createElement("h2", null, "Ada Lovelace"),
  React.createElement("p", null, "First programmer")
);

Either form produces the same React element object. You can confirm this mentally: lowercase tags like div become string types, while capitalized tags like <Welcome /> pass the component function itself as the type.

Who does the compiling

JSX is not valid JavaScript on its own, so a transpiler handles it at build time. Babel (via @babel/preset-react) and SWC (used by Vite and Next.js) are the common choices — both are fast and require zero runtime cost in the browser.

ToolWhere it is usedNotes
BabelCreate React App, Jest, custom setupsMature, plugin-rich
SWCVite, Next.jsRust-based, much faster builds
TypeScript (tsc).tsx filesCan emit JSX directly during type-checking

Modern toolchains default to the automatic runtime (React 17+), which auto-imports a jsx helper from react/jsx-runtime instead of calling React.createElement. That is why you no longer need import React from "react" at the top of every file — the compiler injects the right import for you.

The automatic runtime is enabled by default in Vite, Next.js, and Create React App. If you see a “React is not defined” error, your toolchain is using the legacy classic runtime and needs import React from "react" or a config update.

JSX is not HTML

The resemblance to HTML is intentional but incomplete. Because JSX compiles to JavaScript object keys, attribute names follow DOM property conventions:

  • class becomes className
  • for becomes htmlFor
  • Multi-word attributes are camelCased: tabindex becomes tabIndex, onclick becomes onClick
<label htmlFor="email" className="field-label">Email</label>
<input id="email" type="email" tabIndex={0} onChange={handleChange} />

String values use quotes; any other value — numbers, booleans, objects, functions — goes inside curly braces.

The single-root rule and fragments

Because each component returns one expression, JSX must resolve to a single root element. Returning two siblings at the top level is a syntax error. Wrap them in a parent, or use a Fragment to group them without adding a node to the DOM:

function Row() {
  return (
    <>
      <td>Name</td>
      <td>Email</td>
    </>
  );
}

The <>...</> shorthand is a Fragment. Use the explicit <React.Fragment key={id}> form when you need to attach a key, such as inside a list.

Self-closing tags

Any element without children must be self-closed with a trailing slash — this is stricter than HTML, which tolerates <img> or <br>. JSX requires <img /> and <br />:

function Avatar() {
  return <img src="/ada.png" alt="Ada Lovelace" />;
}

Forgetting the slash on a childless element is a common compile error for newcomers.

Best Practices

  • Let your toolchain handle JSX — never hand-write React.createElement in application code.
  • Rely on the automatic runtime so you can skip the manual React import.
  • Remember the HTML-to-JSX renames: className, htmlFor, tabIndex, and camelCased event handlers.
  • Return a single root; reach for a Fragment instead of an extra wrapper <div> to keep the DOM lean.
  • Always self-close childless elements like <img /> and <input />.
  • Capitalize your component names so the compiler treats them as components, not DOM tags.
Last updated June 14, 2026
Was this helpful?