← All posts

Programming 4 min read

The JSX Series (Part 2): What Happens Under the Hood When Babel Transpiles JSX?

Understanding how Babel transpiles HTML-like syntax into React.createElement calls and plain JavaScript UI objects.

The JSX Series (Part 2): What Happens Under the Hood When Babel Transpiles JSX?

JSX Is Not Magic

In Part 1, we looked at how JSX rescued us from the nightmare of string soup and verbose DOM methods. But how does browser-executable code actually come out of HTML-like syntax inside a JavaScript file?

Browsers don't natively understand JSX. If you feed raw JSX to Chrome or Firefox, you'll get a syntax error. Before your code hits the browser, a build tool (like Babel or SWC) transpiles your JSX into plain, standard JavaScript functions.

Step 1: Transpilation to Function Calls

When you write a JSX element, the compiler translates it into a call to React.createElement() (or the newer _jsx() runtime function).

The Input (What You Write in JSX):

TypeScript


const todoList = ["Learn React", "Learn TypeScript", "Build a React App"];

export const TodoList = () => {
  const name = "Edward Phillips";

  return (
    <>
      <h1>{`${name}'s`} Todo List</h1>
      <ul style={{ listStyleType: "none", padding: 0 }}>
        {todoList.map((item, index) => (
          <TodoItem key={index} item={item} />
        ))}
      </ul>
    </>
  );
};

const TodoItem = ({ item }: { item: string }) => {
  return <li style={{ textDecoration: "none" }}>{item}</li>;
};

The Output (What the Compiler Generates):

JavaScript


import React from "react";

const todoList = ["Learn React", "Learn TypeScript", "Build a React App"];

export const TodoList = () => {
  const name = "Edward Phillips";

  return React.createElement(
    React.Fragment,
    null,
    // <h1> Element with dynamic text children
    React.createElement("h1", null, `${name}'s`, " Todo List"),
    // <ul> Element containing mapped TodoItem components
    React.createElement(
      "ul",
      { style: { listStyleType: "none", padding: 0 } },
      todoList.map((item, index) =>
        React.createElement(TodoItem, { key: index, item: item })
      )
    )
  );
};

const TodoItem = ({ item }) => {
  return React.createElement("li", { style: { textDecoration: "none" } }, item);
};

Understanding React.createElement()

To see what’s happening, look at the signature of React.createElement:

React.createElement(type, props, ...children)

  1. Type: The element to create. This can be a string representing an HTML tag (like "h1" or "ul") or a reference to a custom component function (like TodoItem).

  2. Props: an object containing attributes passed to the element (like { style: { padding: 0 } } or { key: 0, item: "Learn React" }).

  3. Props: Anything nested inside the element—text strings, other React elements, or an array of mapped elements.

Why Component Naming Matters: Capitalization Rules

Have you ever wondered why React component names must start with a capital letter (<TodoItem /> instead of <todoItem />)?

It comes down to how the compiler handles the type argument:

  • Lowercase Tags (<div /> or <li />): The compiler treats these as standard HTML tags and passes them as string literals (React.createElement("div")).

  • Capitalized Tags (<TodoItem />): The compiler treats these as custom React components and passes them as direct variable references (React.createElement(TodoItem)).

The Pitfall: If you name your component todoItem in lowercase, the compiler generates React.createElement("todoItem"). The browser will search for a native HTML element called <todoItem>, won't find one, and your app will fail to render as expected.

Step 2: From Function Calls to JavaScript Objects

Compiling JSX into React.createElement() is only half the journey. What happens when those functions actually execute in the browser?

They return plain, light-weight JavaScript objects that describe the UI structure.

For example, a simple expression like:

JavaScript

const element = <div className="greeting">Hello World</div>;

Transpiles to:

JavaScript

const element = React.createElement("div", { className: "greeting" }, "Hello World");

When executed, it outputs an object structure in memory that looks like this:

JavaScript


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

Key Takeaway

JSX is just a cleaner, visual shortcut. It allows us to write familiar HTML-like code to build nested trees of JavaScript objects, so we don't have to manually type out dozens of repetitive React.createElement() function calls.

Now that we know JSX turns into a tree of plain JavaScript objects in memory, a crucial question arises: What does React actually do with this object tree?

That brings us to Part 3, where we'll explore how these JSX objects power the Virtual DOM and allow React to perform lighting-fast, targeted UI updates.