JSX rules
JSX is not HTML. It compiles to plain JavaScript function calls before it runs, and that origin explains all of its rules. These four rules are worth learning now because they are the first errors every React developer hits: a rejected class attribute, a mysterious "Adjacent JSX elements" message. Knowing why each rule exists turns those errors from roadblocks into thirty-second fixes.
Rule 1: one root element. A function returns one value, so a component must return one element. Wrap siblings in a <div>, or in an empty fragment <>...</> when you do not want an extra DOM node:
function Profile() { return ( <> <h2>Amara</h2> <p>Frontend engineer</p> </> ); }
Rule 2: close every tag. HTML tolerates <br> and <img>. JSX does not, write <br /> and <img />.
Rule 3: curly braces embed JavaScript. Inside JSX, {expression} evaluates any JavaScript expression, a variable, a function call, a ternary:
function Price() { const amount = 19.5; return <p>Total: {amount * 2} dollars</p>; }
That renders Total: 39 dollars. Statements like if or for are not expressions, so they cannot go inside braces. Use ternaries or compute values above the return.
Rule 4: camelCase attributes. Because JSX becomes JavaScript, attribute names follow JavaScript rules. class is a reserved word, so JSX uses className. Also onclick becomes onClick and for becomes htmlFor:
<label htmlFor="email" className="field">Email</label>
What JSX compiles into
JSX like <h1 className="title">Hello, React</h1> becomes a function call that returns a plain object describing the element. Building a tiny version of that function demystifies the whole syntax.
function h(type, props, ...children) { return { type: type, props: props || {}, children: children }; } // <h1 className="title">Hello, React</h1> const el = h("h1", { className: "title" }, "Hello, React"); console.log(JSON.stringify(el)); // <div><h1>Tasks</h1><p>2 items</p></div> const app = h("div", null, h("h1", null, "Tasks"), h("p", null, "2 items")); console.log(app.type); console.log(app.children.length); console.log(app.children[0].children[0]);
Output
{"type":"h1","props":{"className":"title"},"children":["Hello, React"]}
div
2
TasksRead the object printed on the first line, with its type, props, and children. That is all a React element is, meaning a plain JavaScript object with no DOM node inside it and no rendering ability of its own.
The rest-parameter syntax ...children is from Advanced JavaScript, and it is why nesting works so naturally. Every extra argument after props becomes a child, so h("div", null, a, b, c) builds a three-child element with no special syntax.
Note that a child is either a string or another element object, which is what the last two console.log lines demonstrate. app.children[0] is the h1 element, and its own first child is the string "Tasks".
This also explains rule 1 about a single root element. A function returns one value, and h returns one object, so there is nowhere for a second sibling to go at the top level.
Doing react-dom's job
Having built the element objects, the other half is turning a tree of them back into HTML. A string child renders as itself, and an element renders as its opening tag, its rendered children, and its closing tag.
function h(type, props, ...children) { return { type: type, props: props || {}, children: children }; } function render(el) { if (typeof el === "string") return el; const inner = el.children.map(render).join(""); return "<" + el.type + ">" + inner + "</" + el.type + ">"; } const app = h("div", null, h("h1", null, "Tasks"), h("p", null, "2 items")); console.log(render(app)); console.log(render(h("span", null, "plain")));
Output
<div><h1>Tasks</h1><p>2 items</p></div>
<span>plain</span>el.children.map(render).join("") renders the children first and glues them together, and render calling itself is recursion from Advanced JavaScript, needed because a child can contain more elements to any depth.
The string check is the base case, and without it the recursion would try to read .children off "Tasks" and produce undefined. Every recursive function needs a case that stops descending, and here it is the point where a child turns out to be text.
This pair, elements as data plus a renderer that walks them, is the core of what React and react-dom actually do. React builds the objects and react-dom turns them into real DOM nodes rather than a string, and the real version also diffs against the previous tree instead of rebuilding from scratch.
Note that this toy renderer ignores props entirely, so className never reaches the output. Adding it would mean turning the props object into attribute text, which is a small amount of extra code and no new idea.
A rejected attribute name
function Alert() { return ( <h2 class="warning">Low battery</h2> ); }
The bug is class, which should be className, because class is a reserved JavaScript word.
JSX compiles to JavaScript, as the previous blocks showed, so an attribute becomes a key on the props object and class is a keyword there. React warns about it in the console rather than crashing, and the styling silently fails to apply.
The parentheses around the multi-line JSX are fine and are normal style, not part of the bug. They exist so the return and the opening tag can sit on different lines, since JavaScript would otherwise insert a semicolon after return and the function would return undefined.
Two sibling mistakes come from the same rule and are worth memorizing together. onclick becomes onClick, and for on a label becomes htmlFor, because for is a keyword too.
Two roots in one return
function Stats() { return ( <h2>Stats</h2> <p>42 visits</p> ); }
This fails because it returns two sibling elements with no single root, and the fix is to wrap them in a fragment.
A function returns one value, and two side-by-side elements are two values, so JSX rejects it outright with the "Adjacent JSX elements" message. Wrapping gives return <><h2>Stats</h2><p>42 visits</p></>, and the fragment groups them without adding an extra DOM node.
The h function from earlier explains why this is a hard error rather than a warning. Each element is a separate h(...) call, and there is no syntax for returning two of them, so the compiler has nothing to produce.
A <div> wrapper would also compile and is sometimes wrong for a different reason. An extra div shows up in the real DOM and can break CSS layouts that depend on direct-child relationships, such as a flex or grid container, which is exactly the problem fragments were added to solve.