Skip to main content

JSX Deep Dive: Beyond the Basics (Part 2) #17

📖 Introduction

Following our exploration of how JSX is transformed into React.createElement calls, we now turn our attention to the modern innovations that have made JSX even more convenient. For years, a hard rule of React was that React must be in scope to use JSX. However, with recent updates to React and Babel, this is no longer the case.

This article delves into the "new JSX transform," explaining how it works, why it was introduced, and how it streamlines the developer experience by removing the need for explicit React imports in many files.


📚 Prerequisites

Before we begin, please ensure you have a solid grasp of the following concepts:

  • JSX to React.createElement: A clear understanding of the concepts covered in the previous article.
  • Build Tools (Conceptual): A general idea that tools like Babel compile modern JavaScript and JSX into a format that browsers can understand.
  • JavaScript Modules: Familiarity with import and export statements.

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • The Classic JSX Transform: A quick review of the old method and its main drawback.
  • Introducing the New JSX Transform: Understanding the motivation and benefits of the modern approach.
  • Under the Hood: How the new transform automatically imports special functions from react/jsx-runtime.
  • Practical Impact: Seeing why you no longer need to import React from 'react' just to use JSX.
  • Configuration: A brief look at how to enable the new transform in a Babel setup.

🧠 Section 1: The "Old Way" - The Classic JSX Transform

As we learned in the previous article, the original JSX transform took any JSX syntax and converted it into a React.createElement() function call.

The Code:

// component.jsx
function MyComponent() {
return <h1>Hello!</h1>;
}

The Problem: Because the compiled output was React.createElement('h1', null, 'Hello!'), the React variable had to be in scope wherever JSX was used. This led to the ubiquitous import React from 'react'; at the top of almost every component file, even if React wasn't explicitly used for anything else (like Hooks).

This had a few downsides:

  • Slightly More Boilerplate: Every file using JSX needed the import.
  • Learning Curve: It was a source of confusion for beginners who wondered why an seemingly "unused" import was necessary.
  • Performance Limitations: It prevented certain optimizations that the React team wanted to introduce.

💻 Section 2: The "New Way" - The Automatic Runtime

To solve these issues, React (starting in v17) and Babel (starting in v7.9.0) introduced a new JSX transform, often called the "automatic runtime."

The core idea is simple: instead of the developer importing React, the compiler automatically imports the necessary functions for JSX.

Let's look at the same component with the new transform.

The Code (No Change Needed):

// component.jsx
// No import React needed!
function MyComponent() {
return <h1>Hello!</h1>;
}

The New Transformation: The Babel compiler, when configured with the automatic runtime, transforms the code into this:

// component.compiled.js
// Inserted by the compiler, not by you!
import { jsx as _jsx } from 'react/jsx-runtime';

function MyComponent() {
return _jsx('h1', { children: 'Hello!' });
}

Step-by-Step Code Breakdown:

  1. Automatic Import: The compiler injects an import statement at the top of the file. It imports the jsx function from a special entry point: react/jsx-runtime. This file is optimized for production builds.
  2. The jsx() Function Call: Instead of React.createElement(), the compiled code now calls the imported jsx() function (aliased as _jsx to avoid naming conflicts).
  3. The props Object: Notice the arguments are slightly different. The jsx function takes the type ('h1') and a props object that includes the children: { children: 'Hello!' }. This structure is more aligned with how the virtual DOM works internally and allows for future optimizations.

The biggest benefit for developers is clear: you no longer need to import React just to use JSX. You still need to import it to use Hooks (useState, useEffect, etc.) or other utilities, but for simple components that just render markup, the import is gone.


🛠️ Section 3: Handling Multiple Children with jsxs

The new transform is even smarter when it comes to handling multiple children. It has a separate optimized function for when an element has more than one child.

The JSX:

// card.jsx
function Card() {
return (
<div>
<h2>Title</h2>
<p>Some text.</p>
</div>
);
}

The Transformation:

// card.compiled.js
import { jsxs as _jsxs } from 'react/jsx-runtime';
import { jsx as _jsx } from 'react/jsx-runtime';

function Card() {
return _jsxs('div', {
children: [
_jsx('h2', { children: 'Title' }),
_jsx('p', { children: 'Some text.' })
]
});
}

Here, the compiler uses jsxs (the 's' is for static) because the div has multiple, static children. This is a performance optimization that allows React to handle arrays of children more efficiently. You don't need to know the fine details, but it's a key part of why the new transform is better.


🚀 Section 4: Configuring the JSX Transform

In most modern React toolchains like Create React App, Next.js, or Vite, the automatic runtime is enabled by default. You don't need to do anything to use it.

However, if you're setting up Babel manually, you can configure it in your .babelrc or babel.config.json file.

Enabling the Automatic Runtime:

// babel.config.json
{
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic" // This is the key!
}
]
]
}

By setting "runtime": "automatic", you tell Babel to use the new JSX transform. The default, "classic", uses the old React.createElement transform.

You can even specify a different library to provide the JSX functions using the importSource option, which is how other libraries like Preact can leverage JSX.

// For a library other than React
{
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic",
"importSource": "custom-jsx-library"
}
]
]
}

✨ Section 5: Practical Implications and Best Practices

Best Practices:

  • Remove Unused Imports: Since import React from 'react' is no longer needed for JSX, you can safely remove it from files where you are not using any other React exports (like Hooks). There are codemods available to do this automatically across a large codebase.
  • Prefer Named Imports: When you do need to import something from React, prefer named imports. This makes it clearer what your component depends on.

Do this:

import { useState, useEffect } from 'react';

Instead of this:

import React from 'react';
const [state, setState] = React.useState();

💡 Conclusion & Key Takeaways

The new JSX transform is a significant quality-of-life improvement for React developers. It reduces boilerplate, simplifies the learning process, and paves the way for future optimizations.

Let's summarize the key takeaways:

  • The New Transform is Automatic: The compiler injects the necessary imports (react/jsx-runtime) for you.
  • No More import React for JSX: You can write JSX in a file without importing the React library, as long as you don't use other React exports like useState.
  • Two New Functions: The new transform uses jsx() for single children and jsxs() for multiple children, which are optimized for performance.
  • It's the Default: Modern React frameworks use the automatic runtime by default, so you're likely already benefiting from it.

Challenge Yourself: Go back to some of the simple components you wrote in previous lessons. If they don't use any Hooks, try removing the import React from 'react'; line. Thanks to the new JSX transform, your code should still work perfectly!


➡️ Next Steps

Now that you have a complete picture of how JSX works from syntax to final output, we are ready to explore its full power. In the next article, "Embedding JavaScript in JSX", we will dive into using curly braces {} to bring dynamic data, logic, and expressions directly into your markup.

Thank you for your dedication. Stay curious, and happy coding!


glossary

  • JSX Transform: The process, handled by a compiler like Babel, of converting JSX syntax into standard JavaScript function calls.
  • Classic Runtime: The original JSX transform method that compiled JSX to React.createElement() and required React to be in scope.
  • Automatic Runtime: The modern JSX transform that automatically imports jsx and jsxs functions from react/jsx-runtime, removing the need to import React for JSX.
  • react/jsx-runtime: A special entry point in the React library that exports the jsx and jsxs functions used by the automatic runtime.

Further Reading