Skip to main content

Writing Markup with JSX

Why JSX exists​

In React, markup and the logic that renders it live together in one component. The old web split was by technology (HTML / CSS / JS in separate files); React splits by component instead, so a piece of UI and its logic stay in sync on every edit.

The 3 rules of JSX​

JSX looks like HTML but is stricter.

1. One root element — a component returns a single parent. Wrap siblings in a tag or an empty Fragment <>…</>. (Why: JSX becomes JS objects, and a function can't return two objects unwrapped.)

2. Close every tag — <img />, <li>item</li>. No unclosed tags.

3. camelCase attributes — class → className, stroke-width → strokeWidth, for → htmlFor. (Why: attributes become JS object keys, and class is a reserved word.) Exceptions: aria-* and data-* keep their dashes.

For converting a chunk of existing HTML, don't do it by hand — paste it into the HTML → JSX converter; it fixes all three rules automatically.

Under the hood — JSX is not HTML​

JSX is JavaScript, not HTML — it compiles down to plain function calls.

Old ("classic") transform → React.createElement(...). This is why every file needed import React from 'react' even when I never referenced React directly — it had to be in scope:

jsx-compiles.jsx
// I write
<h1>Hello</h1>
// compiles to
React.createElement('h1', null, 'Hello')

New ("automatic") transform (React 17+) → the compiler auto-imports a jsx() helper from react/jsx-runtime, so I no longer need to import React just to use JSX:

jsx-runtime.jsx
import { jsx as _jsx } from 'react/jsx-runtime';
_jsx('h1', { children: 'Hello' })

Notice the lowercase 'h1' passed as a string — that's the same case rule from components: lowercase = host element (string), Capitalized = a component (variable).

Full detail: Introducing the New JSX Transform.