Reading JSX as if it were JavaScript
Status: Draft
I think it's easier to reason about how React works after you learn to look at JSX as if it were JavaScript. In this article, we're taking it apart to see how it actually ends up in the browser.
From JSX to JavaScript
The official page says JSX is a concise and familiar syntax for defining tree structures with attributes. That is, JSX allows us to describe a hierarchy of elements — either native DOM elements, or our own, custom ones, based on function and class components — in a language that looks a lot like HTML but which pre-processors ultimately turn into JavaScript.
JSX is not a part of React per se. See, React itself has a syntax for defining the tree structures with attributes that make up an application. It's the humble createElement
:
React.createElement(type, props, ...children);
We can use it to create a structure of nested elements. But while it's somewhat concise, it's not too familiar. Nesting a bunch of createElement()
statements quickly becomes unreadable, so that's where JSX comes in and lets us write the sort-of-HTML that gets transformed, by way of Babel or a similar transpiler, into React.createElement
statements1. This happens at build-time, so the browser will never know JSX was there in the first place.
You can try out some JSX in the Babel Playground to see what the resulting JavaScript looks like:
// Input:
function Button(props) {
return <button type="button">{props.label}</button>;
}
// Output:
function Button(props) {
return React.createElement(
'button', // type
{ type: 'button' }, // props
props.label // children
);
}
In turn, when the browser executes the calls to React.createElement
, they themselves return something even simpler: plain JavaScript objects that describe our elements. For example, running:
React.createElement(
'button', // type
{ type: 'button' }, // props
'Click me' // children
);
Results in this object:
{
"$$typeof": Symbol(react.element),
// type
type: "button",
// props (including children)
props: {
type: "button",
children: "Click me"
},
// some reserved props
key: null,
ref: null,
// miscellaneous
_owner: null,
_self: null,
_source: null,
_store: {}
}
The source code for
React.createElement
is in ReactElement.js, if you're curious to read it.
Let's unpack this object line by line.
The $$typeof
property identifies the object as a React element. Dan Abramov explains its backstory as a way to improve React's resilience against malicious markup.
Next we see the type
and props
we defined for the element. This is the "meat" of our object and it mostly maps 1:1 to the corresponding createElement
call, with a few exceptions:
key
andref
, which are props with special meaning in React, will not be part of theprops
object. Instead, they get their own properties inside the ReactElement object. They're not part of a component's interface with the world, so you won't be able to access neither thekey
, nor theref
, from inside the component.children
is treated as any other prop on the element. In our example, since we have a single child to the element,props.children
is be a simple value — the stringClick me
. If we had more than one child to the element,props.children
would have been an array.
Finally, React has set up some miscellaneous, underscore-prefixed stuff on the object. Some of these privates are only used in development mode, for warnings and stuff like that, and for the purpose of this article, we'll ignore them.
👉 Ultimately, all the JSX in your application will produce plain, nested JavaScript objects that describe React elements.
How JSX works and what it tells us
The way JSX is transformed to JavaScript can give us some insights on how things work under the hood.
Element type casing
When you write a tag in JSX, the casing of the tag name matters. <button/>
will become { type: 'button' }
, a string identifying it as a native DOM element. On the other hand, a capitalized <Button>
tag will become { type: Button }
, i.e. a reference to the React component your element is based on.
If you want to store the reference to a component in a variable, you need to make the variable capitalized for JSX to pick up on it as a component reference, rather than a native DOM element:
// Input:
function DynamicComponent(props) {
var ActualComponent = props.component;
return <ActualComponent />;
}
// Output:
function DynamicComponent(props) {
var ActualComponent = props.component;
return React.createElement(ActualComponent, null);
}
JSX recognizes element types that have dots in their name, and considers them references to components. So in this particular case, it works even if we write it out directly:
// Input:
function DynamicComponent(props) {
return <props.component />;
}
// Output:
function DynamicComponent(props) {
return React.createElement(props.component, null);
}
However, expressions any more complicated than that, such as <components[props.type] />
, would (presumably) bloat the JSX grammar, and are not supported.
JavaScript inside JSX
JSX allows JavaScript expressions for props, including children
, if you wrap them in curly braces ({}
). It does not, however, allow JavaScript statements. I remember being confused about this when I was starting out because I was trying to look at JSX as if it were a templating language.
Looking instead at how Babel places the expressions in the resulting React.createElement()
word-for-word, with no interpreting whatsoever, clarifies why everything between curly braces needs to make sense as something to assign to an object property, in order to get valid JavaScript.
// Input:
function Button(props) {
return (
<Button type="button" disabled={this.props.disabled}>
{this.props.label}
</Button>
);
}
// Output:
function Button(props) {
return React.createElement(
Button,
{
type: 'button',
disabled: this.props.disabled
},
this.props.label
);
}
You may also notice that any prop set as a string in JSX will remain a string throughout the process — neither Babel, nor React go "oh, this looks like a number, let me convert that for you":
// Input:
const Button = props => <Button tabindex="0" disabled="true" />;
// Output:
const Button = props =>
React.createElement(Button, {
tabindex: '0',
disabled: 'true'
});
Boolean props — the ones which are merely present in name, but don't have a value — are the exception. They show up as boolean true
values in the resulting JavaScript:
// Input:
const Button = props => <Button disabled />;
// Output:
const Button = props => React.createElement(Button, { disabled: true });
Otherwise, you need to pass the props as JavaScript expressions for them to retain the intended type:
// Input:
const Button = props => <Button tabindex={0} disabled={true} />;
// Output:
const Button = props =>
React.createElement(Button, {
tabindex: 0,
disabled: true
});
The element's children
On prop in particular, the children
, has more syntatic sugar going for it in JSX. Anything between the opening tag and the closing tag of an element is split up into text nodes, expressions, and elements, and passed to React as individual children. For example:
// Input:
function Total(props) {
return (
<div>
Sum: {props.a + props.b}
<span>Cool, huh!</span>
</div>
);
}
// Output:
function Total(props) {
return React.createElement(
'div',
null,
// children
'Sum: ',
props.a + props.b,
React.createElement('span', null, 'Cool, huh!')
);
}
We've seen in the previous section that when JSX becomes a JavaScript object, children
are lumped together with the other props in ReactElement's props
object. So there's really nothing stoping us (except common sense, that is) from renouncing this benefit and writing it up as a normal prop with an array value:
function Total(props) {
return (
<div children={['Sum: ', props.a + props.b, <span>Cool, huh!</span>]} />
);
}
The way the children
prop ends up with an array that mixes text nodes, elements, and JavaScript expressions, gives us an extra insight: even when our component employs the conditional rendering of some child, the component still has a fixed number of children. Consider this component:
function Bag(props) {
return (
<div>
This is my bag.
{props.empty && <span>...and it's empty!</span>}
</div>
);
}
Whenever the empty
prop is truth-y, an additional message (and thus, an additional DOM element) is shown. My old JSX-as-templating-language mind would think React has to do some kung-fu to update the DOM when the message needs to be shown or hidden. But it's actually very simple: regardless of what the expression evaluates to when the component is rendered, it still takes up a slot in the children
array:
function Bag(props) {
return React.createElement(
'div',
null,
'This is my bag.',
props.empty && React.createElement('span', null, "...and it's empty!")
);
}
It's just that the slot can be filled with either false
, or a React element, depending on what the expression evaluates to. And since React won't render a false
child, this amounts to adding/removing the child in the second slot from the DOM.
👉 React will not render boolean values (
true
andfalse
),null
, norundefined
, and this makes conditional rendering work.
The spread operator
One last bit of syntactic sugar JSX affords is the spread operator for props, which allows us to endow an element with a set of props without enumerating them one by one, explicitly. In the example below, we want to spread whichever props the Button
component receives to the underlying button
element for some reason:
// Input:
function Button(props) {
return <button type="button" {...props} />;
}
// Output:
function Button(props) {
return React.createElement(
'button',
_extends({ type: 'button' }, props),
'Hello world!'
);
}
In the resulting JavaScript, _extends
is a polyfill Babel introduces for cases where Object.assign
is unavailable.
With Object.assign
(and the polyfill), the order of the objects to extend matters. Overlapping properties in the objects get overwritten from left to right. By the same token, the position of the spread operator in a JSX element matters:
// `type` is potentially overwrittenn
<button type="button" {...props} />
// `type` is always "button"
<button {...props} type="button" />
Conclusion
At the end of the day, JSX is just an nice way to write big-ass JavaScript objects to represent the React elements, and element trees, that make up your application. If you look at a <button>
and remember that when you strip all the onion layers you get an object akin to { type: 'button', props: {} }
, I think many more things in React start to make more sense.
Further reading from the official docs:
1 We're free to:
- not use JSX to write React components;
- use a shorthand other than JSX to write React components;
- transform JSX to something other than
React.createElement
calls. Tell this to Babel by writing/* @jsx some-string */
at the top of the JavaScript file. Try something crazy like/* @jsx λ */
in the Playground to see what happens.