JSX Deep Dive: How React Transforms Markup into Code
JSX is syntactic sugar for React.createElement() calls. When you write <h1>Hello</h1> in JSX, a compiler like Babel converts it into a plain JavaScript object that React uses to construct the DOM. Understanding this transformation is essential for writing efficient, bug-free React components and debugging unexpected rendering behavior.
Key Takeaways
- JSX is not valid JavaScript; Babel or another transpiler must convert it to
React.createElement()function calls React.createElement()returns a lightweight JavaScript object that describes what React should render- Every JSX element, attribute, and child becomes part of a nested object structure
- Using JSX dramatically improves code readability compared to manual
React.createElement()calls - The modern JSX transform (since React 17) no longer requires
import React from 'react'in every file
Prerequisites
Before reading this article, ensure you understand:
- JavaScript Objects and Functions: How to create and manipulate objects and define functions
- React Components: How to write basic functional and class components
- Basic HTML: Standard HTML tags, attributes, and nesting
The Problem JSX Solves
Without JSX, creating a simple user profile card requires nested React.createElement() calls that become unreadable as complexity increases. With JSX, you write HTML-like syntax that is immediately recognizable. This deep dive reveals exactly what happens between the syntax you write and the objects React receives.
How JSX Transforms into React.createElement Calls
A Simple Transformation: From JSX to JavaScript
The simplest JSX example demonstrates the core transformation:
const element = <h1>Hello, world!</h1>;
Babel compiles this into:
const element = React.createElement('h1', null, 'Hello, world!');
The React.createElement() function accepts three arguments:
- type — The element type: a string for HTML tags (
'h1','div') or a reference to a React component - props — An object containing attributes (e.g.,
className,id,onClick);nullif no props exist - children — One or more children (nested elements or text); can be multiple arguments
When executed, React.createElement() returns a JavaScript object:
{
"type": "h1",
"props": {
"children": "Hello, world!"
},
"key": null,
"ref": null
}
React reads this object (called a "React element") to know what to render on screen.
Nested Elements and Props
Adding props and children creates a more complex transformation:
const element = (
<div className="greeting">
<h1>Hello!</h1>
<p>Welcome to our app.</p>
</div>
);
Babel converts this to nested React.createElement() calls:
const element = React.createElement(
'div',
{ className: 'greeting' },
React.createElement('h1', null, 'Hello!'),
React.createElement('p', null, 'Welcome to our app.')
);
Each child element becomes a separate argument to the parent's React.createElement() call. This nesting mirrors the hierarchical structure of your JSX, but in function-call syntax.
Building a Complete Example: User Profile Card
Let's build a realistic component and examine both the JSX and its compiled equivalent.
import React from 'react';
function UserProfile() {
const user = {
name: 'Hedy Lamarr',
imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',
bio: 'Inventor and actress'
};
return (
<div className="profile-card">
<img
className="avatar"
src={user.imageUrl}
alt={'Photo of ' + user.name}
style={{
width: 100,
height: 100,
borderRadius: '50%'
}}
/>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
}
export default UserProfile;
The compiled version (what Babel produces) is:
import React from 'react';
function UserProfile() {
const user = {
name: 'Hedy Lamarr',
imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',
bio: 'Inventor and actress'
};
return React.createElement(
'div',
{ className: 'profile-card' },
React.createElement('img', {
className: 'avatar',
src: user.imageUrl,
alt: 'Photo of ' + user.name,
style: {
width: 100,
height: 100,
borderRadius: '50%'
}
}),
React.createElement('h1', null, user.name),
React.createElement('p', null, user.bio)
);
}
export default UserProfile;
This demonstrates the verbosity and unreadability that JSX solves. The side-by-side comparison makes clear why JSX is the idiomatic way to write React.
Using Custom Components in JSX
The real power of React composition emerges when you use custom components as types. When you pass a component to JSX, it becomes the type argument:
import UserProfile from './UserProfile';
function App() {
return <UserProfile />;
}
Compiles to:
import UserProfile from './UserProfile';
function App() {
return React.createElement(UserProfile, null);
}
React detects that UserProfile is a function (or class), not a string, and invokes it to render its content. This is the mechanism that enables the entire component composition model.
Best Practices for Working with JSX
Do: Embrace JSX Syntax
JSX is the idiomatic, recommended way to write React. Use it consistently across your codebase.
// Good
return <div className="container">{content}</div>;
Do: Use Parentheses for Multi-Line JSX
Wrapping multi-line JSX in parentheses prevents JavaScript's automatic semicolon insertion from breaking your code:
// Good
return (
<div>
<h1>Title</h1>
<p>Content</p>
</div>
);
Do: Extract Complex Logic Outside JSX
Keep your JSX readable by moving complex conditionals or loops outside the return statement:
// Good
let content;
if (userIsLoggedIn) {
content = <p>Welcome back!</p>;
} else {
content = <p>Please log in.</p>;
}
return <div>{content}</div>;
// Avoid
return (
<div>
{userIsLoggedIn ? <p>Welcome back!</p> : <p>Please log in.</p>}
</div>
);
Avoid: Immediately Invoked Functions in JSX
Do not use IIFE (Immediately Invoked Function Expressions) inside JSX; it reduces readability and performance:
// Avoid
return (
<div>
{(() => {
if (conditionA) return <p>A</p>;
if (conditionB) return <p>B</p>;
return <p>C</p>;
})()}
</div>
);
The Role of Babel in JSX Transformation
Babel is a JavaScript transpiler that converts JSX (and other modern syntax) into standard JavaScript that browsers understand. It parses your JSX code, identifies JSX expressions, and generates equivalent React.createElement() calls. You can see Babel's transformation in real-time using the Babel REPL.
The modern JSX transform (introduced in React 17) automatically imports the JSX factory function, allowing you to omit import React from 'react' in files that only use JSX without directly calling React methods.
Frequently Asked Questions
What happens to JSX attributes with dashes, like data-testid?
HTML attributes with dashes (like data-* attributes) are passed through as-is to the props object. In JSX, you write data-testid="my-id" and it becomes { 'data-testid': 'my-id' } in props. Only class becomes className because class is a reserved JavaScript keyword.
Can I use JSX without React?
Yes. JSX is a syntax extension that any framework can transpile. Other libraries (Preact, Inferno, Vue) have their own JSX transpilers that compile to their respective APIs. You are not locked into React when you use JSX.
Why does Babel convert className instead of class?
In JSX, you write className because class is a reserved keyword in JavaScript. Babel converts className to the class attribute when rendering to actual DOM. This avoids syntax errors while maintaining familiar HTML-like syntax.
How does Babel handle JavaScript expressions inside JSX?
Curly braces { } in JSX signal to Babel that the content is a JavaScript expression, not a string. Babel extracts the expression and places it as a variable in the props object or as a child argument. For example, <p>{user.name}</p> becomes React.createElement('p', null, user.name).
Is understanding JSX transformation necessary for daily React development?
Understanding the transformation helps you debug rendering issues, avoid common pitfalls (like passing objects as keys), and write more efficient code. However, most developers use JSX idiomatically without consciously thinking about the underlying React.createElement() calls. This knowledge becomes valuable when optimizing performance or debugging unexpected behavior.