Skip to main content

JSX Transform: Automatic Runtime Guide

React's automatic JSX transform, introduced in React 17, eliminates the need for explicit React imports in files using JSX by automatically injecting the necessary compiler functions. This modern approach improves developer experience and enables future optimizations through streamlined compilation to either jsx() or jsxs() functions from react/jsx-runtime.

Key Takeaways

  • No More React Imports for JSX: The compiler automatically injects JSX runtime functions, removing boilerplate.
  • Classic vs. Automatic: Classic runtime (pre-React 17) required React.createElement; automatic runtime uses optimized jsx/jsxs functions.
  • Performance Optimization: The automatic runtime distinguishes between single-child (jsx) and multi-child (jsxs) scenarios for better performance.
  • Babel 7.9.0+ Required: Ensure your build tool is configured with the automatic runtime setting.
  • Still Import React for Hooks: Even with automatic JSX transform, import React when using useState, useEffect, or other utilities.

Prerequisites

Before diving into this article, ensure you understand:

  • JSX Compilation Basics: How JSX syntax converts into JavaScript function calls (covered in Part 1).
  • Build Tools: A general familiarity with Babel and how it transforms modern JavaScript.
  • JavaScript Modules: Comfort with import and export statements.

What is the Classic JSX Transform?

The original JSX transform, required until React 17, compiled JSX into React.createElement() calls. Any file using JSX had to include import React from 'react' at the top, even if React wasn't used for anything else. This pattern created unnecessary boilerplate and confused developers about why an "unused" import was essential.

// Original JSX
function MyComponent() {
return <h1>Hello!</h1>;
}

// Compiled to (classic runtime)
React.createElement('h1', null, 'Hello!')

Why This Mattered: Because the compiled output referenced React directly, the variable had to be in scope. This was problematic for several reasons:

  • Every JSX file required the import, adding boilerplate.
  • Beginners struggled to understand why the import was necessary if unused.
  • It prevented certain compiler optimizations the React team wanted to introduce.

How Does the Automatic Runtime Work?

Introduced in React 17 and Babel 7.9.0, the automatic runtime solves this by having the compiler itself inject the necessary imports. Instead of React.createElement, the compiler uses optimized functions from react/jsx-runtime.

// Same JSX code
function MyComponent() {
return <h1>Hello!</h1>;
}

// Compiled to (automatic runtime)
import { jsx as _jsx } from 'react/jsx-runtime';

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

Step-by-Step Transformation:

  1. Automatic Injection: Babel detects JSX and automatically adds the import from react/jsx-runtime (you don't write this).
  2. Function Call: JSX compiles to _jsx() (or _jsxs() for multiple children) instead of React.createElement().
  3. Optimized Props: The props object includes children, aligning with React's internal virtual DOM structure and enabling future optimizations.

The biggest benefit: no more required React imports for JSX alone. You still import React for Hooks and other utilities, but simple components render without it.

Single vs. Multiple Children: jsx vs. jsxs

The automatic runtime includes a smart optimization: it uses jsxs() (the 's' stands for "static") when a component has multiple children, and jsx() for single children. This distinction allows React to optimize how arrays of children are handled.

// Single child - uses jsx
function Simple() {
return <div>Hello</div>;
}

// Compiled to:
import { jsx as _jsx } from 'react/jsx-runtime';
_jsx('div', { children: 'Hello' });
// Multiple children - uses jsxs
function Card() {
return (
<div>
<h2>Title</h2>
<p>Content</p>
</div>
);
}

// Compiled to:
import { jsxs as _jsxs, jsx as _jsx } from 'react/jsx-runtime';
_jsxs('div', {
children: [
_jsx('h2', { children: 'Title' }),
_jsx('p', { children: 'Content' })
]
});

This dual-function approach allows the React team to optimize each scenario separately, which is why the automatic runtime is more efficient than the classic approach.

Configuring the Automatic Runtime in Babel

Most modern frameworks (Create React App, Next.js, Vite) enable the automatic runtime by default. If you're configuring Babel manually, enable it in your .babelrc or babel.config.json:

{
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
}

The runtime option can be set to:

  • "automatic" — Modern approach (React 17+, Babel 7.9.0+)
  • "classic" — Legacy approach using React.createElement

You can also customize which library provides JSX functions using importSource:

{
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic",
"importSource": "preact"
}
]
]
}

This allows non-React libraries (like Preact) to use JSX with their own optimized runtime.

Best Practices for Modern JSX Development

Remove Unused React Imports: Since JSX no longer requires import React, delete it from files that don't use other React exports (Hooks, utilities, etc.). Many codebases have automated codemods to do this in bulk.

Prefer Named Imports: When importing from React, use named imports to clarify dependencies:

// Good: clear what you're using
import { useState, useEffect } from 'react';

function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {}, []);
return <div>{count}</div>;
}
// Avoid: less clear
import React from 'react';

function Counter() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {}, []);
return <div>{count}</div>;
}

Understand the Difference: JSX still requires the automatic runtime to be enabled in your build configuration. It's not a language feature; it's a compiler feature provided by Babel.

Frequently Asked Questions

Do I Still Need to Import React to Use JSX?

No, not with the automatic runtime. The compiler injects the necessary imports from react/jsx-runtime for you. However, you must still import React if you use Hooks (useState, useEffect) or other React utilities. The automatic runtime only eliminates the import requirement for JSX syntax itself.

What's the Performance Impact of the Automatic Runtime?

The automatic runtime is more performant than the classic approach. By separating single-child (jsx) from multi-child (jsxs) scenarios, React can optimize each case independently. File bundle sizes may also be slightly smaller since fewer React imports are needed in many files.

Can I Mix Classic and Automatic Runtimes?

No, you configure the JSX runtime globally in your Babel configuration. However, you can have some files that don't use JSX and thus don't depend on this setting. Once configured, all JSX in your project will compile to the same runtime.

What If My Build Tool Doesn't Support the Automatic Runtime?

If you're using an older build tool (Webpack 4 without modern Babel, older Jest configurations, etc.), you may need to upgrade or configure Babel manually. Modern toolchains (Webpack 5+, Vite, Parcel 2+, Next.js 12+) all support it by default. Check your tool's documentation for the specific Babel preset configuration needed.

Is the Automatic Runtime Used in Libraries Published to npm?

Library authors must compile their JSX to code that works in any consumer's environment. Most publish to ES5 or ES2015 with pre-compiled JSX. The automatic runtime is primarily a development-time convenience. When publishing, libraries typically configure their build to output compatible JavaScript that doesn't rely on the consumer having the automatic runtime enabled.

Further Reading