Skip to content

Step 1 of 5 · Reading · ~3 min

Read

Reactive Foundations

JSX to createElement Compilation

JSX is not a runtime feature. It is syntax that a build tool removes before the browser sees the code. After this lesson your program does that job for real: it reads JSX source, possibly several lines of nested elements, and prints the factory calls it compiles to.

The mapping

<div className="card" id={nextId}>Hello</div>

becomes

h("div", {className: "card", id: nextId}, "Hello")

The element name is the first argument, the attributes become one props object, and every child is a further argument. h is just the function name the compiler is configured to emit (the pragma: React.createElement, h in Preact). React 17 added an automatic mode that emits calls into a runtime module, so files no longer need to import React, but the shape is the same. Nothing parses JSX at runtime; the transform is purely lexical.

The rules

SourceOutput
<div> (lowercase)the string "div"
<Button> (capital)the bare identifier Button, a reference to your component
<>...</>the identifier Fragment, a wrapper with no element of its own
no attributesnull as the props argument, not {}
a="x" or a='x'a: "x"
a={expr}a: expr, the expression copied as written
bare aa: true
data-id="7""data-id": "7", quoted because it is not an identifier
<br />h("br", null), no children
{expr} as a childexpr, copied as written
nested <b>..</b>a nested call

An expression is copied between its balanced braces without being understood, so a stray > inside {() => go()} must not end the tag, and style={{color: "red"}} works because the outer braces are the JSX container and the inner pair is an object literal. A real compiler parses the expression as JavaScript and transforms JSX inside it too; yours may treat the text as opaque and may assume no brace appears inside a string literal.

The whitespace rule

Text between tags is not copied verbatim. Pretty-printing JSX over several lines must not add stray spaces, but Hello <b>x</b> must keep the space. The rule, as Babel implements it:

  1. Split the text into lines.
  2. Every line except the first loses its leading spaces; every line except the last loses its trailing spaces.
  3. Lines that are now empty are dropped.
  4. The remaining lines are joined with a single space.

A text on one line is untouched, so Hello keeps its trailing space and a lone space between two elements survives as the child " ". Indented source:

<p>
  Hello
  world
</p>

gives the single child "Hello world", and the blank-looking runs between the <li> lines of a list give no child at all. Text is emitted as a string literal, so " and \ inside it are escaped.

How a parser reads it

One function handles one element, entered at <. It reads the name, then attributes until > or />, then children until </, and each child is one of three things: another element (call the same function), an expression container (read to the matching brace), or text (read to the next < or {). Nesting is handled by the call stack, which is the stack of open elements.

Common mistakes

  • Printing {} for no attributes. The convention is null.
  • Quoting a component name ("Button"), which makes the compiler look up an HTML tag.
  • Trimming single-line text, losing the space in Hello <b>x</b>.
  • Ending an expression at the first >, which () => contains.
  • Forgetting to escape " in text or in an attribute string.

Your exercise

The starter reads the source and provides quote and prop_key. You write clean_text, which applies the whitespace rule to one run of text, and transform, the recursive reader that produces the call. Expressions are opaque and never contain <.

Up nextDiffing AlgorithmVirtual DOM & Diffing

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…