What does JSX compile to?

8 min read

React provides two mechanisms to compile JSX.

The first is the old-school react transform. JSX tags are transformed into calls to a function that takes two or more arguments: component, props, and ...children. props must be present, although it can be se t to null if no props are provided. children can be undefined. Multiple children can be specified by passing more arguments.

In TypeScript, this transform makes use of the jsxFactory and jsxFragmentFactory configuration options. jsxFactory can be changed on a per-file basis through the use of a @jsx [pragma] comment.

This mechanism is the reason why ESLint warns you if you do not import React. Your jsxFactory must be in scope for your component to render.

When working with React, jsxFactory will be React.createElement and jsxFragmentFactory will be React.Fragment.

Other libraries can demand the use of different factory functions, or make use of bundlers to rewrite imports of React to that library. When using Preact, you have the option of either using its h function (named after Hyperscript), or using a tool like Webpack to alias React to Preact.

A newer transform was introduced in late 2020 with React 17 and also backported to React 16, 15, and 0.14. Its main benefit is that React no longer needs to be in scope. Compilers instead implicitly import a jsx function from a set module (configurable). The parameters have also been changed.

In TypeScript, this transform instead makes use of the jsxImportSource option, defaulting to react/jsx-runtime. Fragment will also be imported from this module if needed.

Throughout this article, both the react and react-jsx transforms will be shown (the jsx import is omitted for brevity). The React team suggests using React.createElement when making use of React without JSX.

createElement performs validation and creates an object describing the element you passed into it, along with some extra keys. For example:

{ "$$typeof": Symbol.for("react.element"), type: "div", key: null, ref: null, props: { className: 'border', children: 'Hello World' }, _owner: null }

$$typeof is used alongside a Symbol to prevent XSS. _owner is internal to React. type refers to the DOM element or React component. key and ref are props with special handling. props includes children.

The simplest example is a self-closing (childless) element:

<br />
React.createElement("br", null);
jsx("br", {});

When you provide props, the second argument becomes an object:

<img alt="Image" />
React.createElement("img", { alt: "Image" });
jsx("img", { alt: "Image" });

Children are just a prop. When using React.createElement, they are passed as a the third and subsequent arguments. react-jsx instead puts everything into the children prop.

<div>Hello World</div>
React.createElement("div", null, "Hello World");
jsx("div", { children: "Hello World" });

React sees multiple children as an array of elements. When using createElement, all arguments after the second are combined into a children array. The react-jsx transform makes use of a jsxs function that expects children as an array. This makes it more error-prone to work with.

<ul reversed>
  { reversed: true },
  React.createElement("li", null, "Hello"),
  React.createElement("li", null, "World")
jsxs("ul", { reversed: true, children: [jsx("li", { children: "Hello" }), jsx("li", { children: "World" })] });
// React creates { type: "ul", props: { children: [{ type: "li", ...}, ...] } }

JSX supports the ES2018 object spread syntax. With ...object, keys from object are merged into props:

<div className="border" {...props}>
  Hello World

You’ll probably have to scroll to view this snippet. Try to imagine what the JSX should look like based on the compiled output before peeking.

React.createElement("div", { className: "border", ...props }, "Hello World");
jsx("div", { className: "border", ...props, children: "Hello World" });

When a spread is exclusively passed to an element, the behavior depends on your compiler:

<div {...props} />
// Babel compiles this as:
React.createElement("div", props);
jsx("div", props);
// TypeScript, ESBuild, and SWC compile this as:
React.createElement("div", { ...props });
jsx("div", { ...props });

React has an optimization where if the props object has the same identity (only possible when passing by reference, which is what Babel does), it won’t rerender that component whatsoever.

A key strength of React is that it’s just JavaScript. Interpolation can use any JS construct:

<div className="border">{"Hello World".split("").map((s) => s)}</div>
  { className: "border" },
  "Hello World".split("").map((s) => s)
jsx("div", { className: "border", children: "Hello World".split("").map((s) => s) });

Children can also be passed as a prop: children is not reserved in any way by JSX. Although this will compile slightly differently when React.createElement, the end result will be the same as can be seen by the react-jsx output.

<div className="border" children={"Hello World".split("").map((s) => s)} />
React.createElement("div", { className: "border", children: "Hello World".split("").map((s) => s) });
jsx("div", { className: "border", children: "Hello World".split("").map((s) => s) });

Keys are special cased by the react-jsx transform. They must be supplied as the third argument. The ref prop is not handled differently.

<input key={} ref={inputRef} />
React.createElement("input", { key:, ref: inputRef });
jsx("input", { ref: inputRef },;

Fragments are compiled to refer to React.Fragment or additionally import Fragment from the react-jsx importSource

<>Hello World</>
React.createElement(React.Fragment, null, "Hello World");
jsx(Fragment, { children: "Hello World" });

When specifying a key, React.Fragment must be referenced explicitly:

<React.Fragment key="k">Hello World</React.Fragment>
React.createElement("input", { key:, ref: inputRef });
jsx("input", { ref: inputRef },;

Components are passed by reference:

<Button onClick={handleClick}>Subscribe</Button>
React.createElement(Button, { onClick: handleClick }, "Subscribe");
jsx(Button, { onClick: handleClick, children: "Subscribe" });

This makes it possible to dynamically render DOM elements, too. Any in-scope variable may be referenced as long as it starts with a capital letter, or you use dot notation to access a nested property. Because React is just JavaScript, if this variable is a string, it will be treated like a DOM element.

const Component = isNative ? "button" : Button;
<Component onClick={handleClick}>Subscribe</Component>;

const props = { as: "button" };
< onClick={handleClick}>Subscribe</>;
const Component = isNative ? "button" : Button;
React.createElement(Component, { onClick: handleClick }, "Subscribe");
jsx(Component, { onClick: handleClick, children: "Subscribe" });

const props = { as: "button" };

React.createElement(, { onClick: handleClick }, "Subscribe");
jsx(, { onClick: handleClick, children: "Subscribe" });

Lower-case tags that do not use dot notation are always treated as native DOM elements. <tag>Text</tag> would try to create a <tag> element.