DEV Community

Vishal Desai
Vishal Desai

Posted on

React 19 internals

Thumbnail Img: React Diagram

When I first got introduced to React, it felt like magic. I was just describing what I wanted, and somehow React handled all the heavy lifting.

Take a simple example — showing text when a button is clicked. In React, that’s it:

import { useState } from 'react'

function App() {
  const [visible, setVisible] = useState(false);

  return (
     <div>
      <button onClick={ ()=>setVisible(true) }> Click to see </button>
       { visible && <p> Hi, Its Visible </p> }
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

No getElementById. No addEventListener. No manually toggling .style.display. In vanilla JS you're telling the browser how to do something step by step. In React you just declare what the UI should look like for a given state, and React figures out the rest. That's the heart of declarative programming.

I had so many questions. How is this even possible? How does it know what to update and when? So I dug into the React source code — and here’s the thing: it’s not magic at all. It’s a brilliantly designed architecture. A few key ideas working together:

JSX

Remember when you first got introduced to React and saw that .jsxextension? Coming from .html, .css, and .js, it felt strange. What even is this thing?

The short answer: JSX stands for JavaScript XML, and yes — it’s a mixture of HTML and JavaScript living in the same file. But here’s something that might surprise you — the .jsx extension itself is just a convention. You could name the file .xyz and it would still work. React doesn't care about the filename; it cares about what's inside. More on that in a bit.

What’s actually inside a .jsx file is simpler than it looks. At its core, it's just a JavaScript function that returns something that looks like HTML:

function Greeting() {
  const name = "Vishal";

  return (
    <h1>Hello, {name}!</h1>
  );
}
Enter fullscreen mode Exit fullscreen mode

Because it’s a regular function, you can declare variables with const or let at the top. And to use those variables inside the markup, you drop them into curly braces {}. Those curly braces are your escape hatch from HTML-land back into JavaScript anything you put inside them gets evaluated as a JS expression and rendered in the browser.

So {name} doesn't print the literal text "name" — it prints "Vishal". That's the trick. You're not writing HTML, you're writing JavaScript that describes HTML, which is why the two can coexist so naturally in the same file.

Babel

Earlier I mentioned that .jsx is just a convention, you could name your file .xyz and it would still work. But why does it work? That's where Babel comes in.

Babel is a JavaScript compiler/transpiler. Its primary job is to take code that browsers don’t understand and transform it into code they do. It does this in a few important ways:

  • Converts .jsx syntax into plain JavaScript
  • Transpiles modern ES6+ code down to older versions for browser compatibility
  • Works in reverse too — it can handle CommonJS and convert it to ES Modules, or vice versa

So in short, Babel is your compatibility layer. It bridges the gap between the JavaScript you want to write and the JavaScript browsers can actually run.

Now, back to the .jsx vs .xyz example. When Babel processes your project, it needs to know which files to look at. By default it watches for .jsx files. If you rename your files to .xyz, you have to explicitly tell Babel to pick those up instead. In Vite, that's done through a plugin — babel-plugin-react-compiler — where you configure the input file extensions you want Babel to process.

Something like this in your vite.config.js:

export default defineConfig({
  resolve: {
    extensions: ['.xyz', '.js', '.jsx', '.ts', '.tsx'],
  },
  esbuild: {
    loader: 'jsx',
    include: /\.xyz$/,
  },
  plugins: [
    react({
      include: /\.(xyz)$/,
      babel: {
        plugins: ['babel-plugin-react-compiler'],
      },
    }),
  ],
})
Enter fullscreen mode Exit fullscreen mode

But here’s the practical takeaway: stick with .jsx. The convention exists for a good reason every tool in your ecosystem, from your editor to your linter to your bundler, already knows what .jsx means. Changing it just to prove it works is a fun experiment, but in a real project it only creates friction.

What Babel actually compiles to

Okay, fun part over. Let’s look at what Babel is actually producing under the hood.

Take our earlier example:

import { useState } from 'react'

function App() {
  const [visible, setVisible] = useState(false);

  return (
     <div>
      <button onClick={ ()=>setVisible(true) }> Click to see </button>
       { visible && <p> Hi, Its Visible </p> }
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

— — — — — — — — Babel Compiles this to — — — — — — — — - — —

import { useState } from "react";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function App() {
  const [visible, setVisible] = useState(false);
  return _jsxs("div", {
    children: [
      _jsx("button", {
        onClick: () => setVisible(true),
        children: " Click to see "
      }),
      visible &&
         _jsx("p", {
          children: " Hi, Its Visible "
        })
    ]
  });
}
export default App; 
Enter fullscreen mode Exit fullscreen mode

Click here if you want to try.

It looks intimidating at first. But once you understand two things, it all clicks.

What are _jsx and _jsxs?

These are function calls from the react/jsx-runtime package and this is exactly where the React package enters the picture. Your JSX doesn’t magically render itself, Babel transforms every element into one of these function calls, and React takes it from there.

The signature is straightforward:

_jsx(element, { props,children })

  • The first argument is the element type — "div", "button", "p", or a component name
  • The second argument is an object containing its props — things like onClick , style , className
  • children is a special prop that holds everything nested inside the element

What’s the difference between _jsx and _jsxs?

This is the subtle part. The s in _jsxs stands for static — or more practically, it signals that the element has multiple children. _jsx is used when there's a single child.

Look at our example again. The

uses _jsxs because it has two children the <button> and the {visible && <p>} expression. That JavaScript expression inside curly braces isn't just some passive text,React treats it as a child element too.

The <button> itself uses _jsx because it has only one child: its text content.

So the rule is simple:

| Function        |  When it's used                |
---------------------------------------------------
| _jsx            |  Element has a single child    |
| _jsxs           |  Element has a multiple child  |

This is why understanding Babel’s output matters. That JSX you write isn’t HTML — it’s syntactic sugar that gets compiled into plain function calls. React never sees your angle brackets. It only ever sees _jsx and _jsxs.

React Element

Now comes the final piece what the React core library actually works with.

After Babel transforms your JSX into _jsx() and _jsxs() function calls, those functions return a plain JavaScript object called a React Element. This is the lowest level of React.

Here’s what our example looks like at this stage:

import { useState } from "react";

function App() {
  const [visible, setVisible] = useState(false);

  return {
    $$typeof: Symbol.for("react.element"),
    type: "div",
    key: null,
    ref: null,
    props: {
      children: [
        {
          $$typeof: Symbol.for("react.element"),
          type: "button",
          key: null,
          ref: null,
          props: {
            onClick: () => setVisible(true),
            children: " Click to see "
          },
          _owner: null,
        },
        visible && {
          $$typeof: Symbol.for("react.element"),
          type: "p",
          key: null,
          ref: null,
          props: {
            children: " Hi, Its Visible "
          },
          _owner: null,
        }
      ]
    },
    _owner: null,
  };
}

export default App;

The structure is self-explanatory for the most part — type tells React what element to render, props carries its attributes and children, key and ref are special React identifiers, and _owner tracks which component created this element internally.

But the field worth a deep look is $$typeof.

$$typeof — the identity tag

Every React Element carries a $$typeof field. It's a Symbol — a globally unique, unforgeable value and React uses it to identify exactly what kind of thing this element represents. It's also a security mechanism: because Symbols can't be serialized to JSON, a malicious server can't inject a fake React element through a JSON response.

Here are all the types of $$typeof:

  • Symbol.for("react.element")

The most common one. Marks any standard JSX element a <div>, a <button>, or your own component like <App />.

const el = <h1>Hello</h1>;
// $$typeof: Symbol.for("react.element")
// type: "h1"
  • Symbol.for("react.fragment")

Marks a<>...</> or <React.Fragment>. Lets you return multiple elements from a component without adding an extra wrapper node to the DOM.

return (
  <>
    <h1>Title</h1>
    <p>Subtitle</p>
  </>
);
// $$typeof: Symbol.for("react.fragment")

- Symbol.for("react.portal")

Marks a ReactDOM.createPortal(). Renders children into a DOM node that exists outside the parent component's DOM hierarchy commonly used for modals, tooltips, and dropdowns.

return ReactDOM.createPortal(
  <div className="modal">I'm outside the root!</div>,
  document.getElementById("modal-root")
);
// $$typeof: Symbol.for("react.portal")
  • Symbol.for("react.strict_mode")

Marks <React.StrictMode>. A development-only wrapper that activates extra warnings double-invoking render functions, detecting deprecated APIs, and highlighting potential side effects.

<React.StrictMode>
  <App />
</React.StrictMode>
// $$typeof: Symbol.for("react.strict_mode")
  • Symbol.for("react.context")

Marks the object returned by React.createContext()specifically the context object itself that holds the current value.

const ThemeContext = React.createContext("light");
// ThemeContext.$$typeof: Symbol.for("react.context")
  • **Symbol.for("react.provider")**

Marks the <Context.Provider> wrapper. When you wrap components in a Provider and pass a value, React uses this symbol to identify that it's a provider element and should push that value down the tree.

<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>
// $$typeof: Symbol.for("react.provider")
  • Symbol.for("react.memo")

Marks a component wrapped in React.memo(). Tells React to skip re-rendering this component if its props haven't changed a pure performance optimization.

const Button = React.memo(function Button({ label }) {
  return <button>{label}</button>;
});
// $$typeof: Symbol.for("react.memo")
  • Symbol.for("react.lazy")

Marks a component wrapped in React.lazy(). Tells React this component should be loaded dynamically code-split and fetched only when it's first needed.

const Settings = React.lazy(() => import("./Settings"));
// $$typeof: Symbol.for("react.lazy")
  • Symbol.for("react.suspense")

Marks <React.Suspense>. Works hand-in-hand with lazy it defines a boundary where React should show a fallback UI while waiting for a lazy component to load.

<React.Suspense fallback={<p>Loading...</p>}>
  <Settings />
</React.Suspense>
// $$typeof: Symbol.for("react.suspense")
  • Symbol.for("react.forward_ref")

Marks a component wrapped in React.forwardRef(). Allows a parent component to pass a ref down through to a DOM element inside a child component — something React normally blocks.

const Input = React.forwardRef((props, ref) => (
  <input ref={ref} {...props} />
));
// $$typeof: Symbol.for("react.forward_ref")

The full picture

Here’s a quick reference:

$$typeof            |               What it marks
-----------------------------------------------------------------
react.element       | Any standard JSX element or component
react.fragment      | <>...</> — multiple children, no DOM wrapper
react.portal        | createPortal() — renders outside the root
react.strict_mode   | <StrictMode> — dev warnings only
react.context       | The context object from createContext()
react.provider      | <Context.Provider value={...}>
react.memo          | memo() — skips re-render if props unchanged
react.lazy          | lazy() — dynamic import, code splitting
react.suspense      | <Suspense> — fallback while lazy loads
react.forward_ref   | forwardRef() — passes ref to a child DOM node

Every element React ever deals with carries one of these symbols. When React walks the tree to figure out what to render, update, or skip it’s reading $$typeof first to know what it's looking at, and then deciding what to do with it.


Here is React core package actually looks.

React Folder structure


Final thoughts

That React Element tree gets handed off to React DOM, which takes over rendering, committing those elements to the actual browser DOM. And sitting between the two is where React Fiber and the reconciliation algorithm live, the engine that figures out the minimum set of changes needed to keep your UI in sync with your state.

That’s a deep enough rabbit hole to deserve its own blog post, and it’s coming soon.

If this breakdown helped something finally click for you, drop a like and leave a comment below. And if something didn’t land clearly, ask — genuinely happy to dig into it further. More React,React Native,Nextjs, Tanstack internals on the way, so follow along if you don’t want to miss it.

— — — — — — — — — — — -—-The End — — — — — — — — — — — — —— —

Thank you

“Build things that you want to see” — Vishal Desai

Top comments (0)