A Beginner's Guide to the React Compiler

Introduction

The field of web development is in a perpetual state of flux, with a plethora of libraries and technologies continually emerging within the ecosystem. Among these, React stands out as one of the most popular libraries for web development.

While periodic minor updates are the norm, the React team has made a significant announcement this year with the unveiling of React 19. The beta version of React 19 was officially released to the public on April 25, 2024, and it's packed with a host of exciting new features.

One feature that particularly caught my eye is the introduction of the React Compiler. Sounds really fancy is what I thought, but its functionality is even more impressive. Previously, we relied on hooks like useMemo and useCallback for memoization to optimize our code. These hooks help React minimize unnecessary updates by indicating which parts of our application don't need to recompute if their inputs haven't changed. While effective, these memoization techniques can be easy to forget or misapply, leading to redundant updates as React checks unchanged areas of your user interface (UI).

Enter the React Compiler. This nifty addition automates the memoization process, ensuring optimal performance without the hassle. So, buckle up as we delve into the wonders of React 19 and explore how this compiler is set to revolutionize our development experience. As per the official documentation,

The compiler uses its knowledge of JavaScript and React’s rules to automatically memoize values or groups of values within your components and hooks. If it detects breakages of the rules, it will automatically skip over just those components or hooks, and continue safely compiling other code.

So let's take a look at how it works.

Installing React 19

React Compiler requires React 19 RC. While it is possible to use it without upgrading to React 19, it involves many workarounds. Let's keep it simple for now.

Since React 19 is still in the beta phase, the installation steps are a bit different from usual. We will use Vite for our setup.

First we navigate to the folder containing all of our repos and run the following command in a terminal:

npm create vite@latest react-compiler-test

Make sure to choose React with either JavaScript or TypeScript when prompted. I prefer JavaScript, so I will choose it. The new folder created by this command will be named react-compiler-test (you can change this name in the command if you want).

Navigate into that folder:

cd react-compiler-test

Then run the following command:

npm install react@beta react-dom@beta

After this command finishes, run:

npm install

Now to start the application, run:

npm run dev

This will start our React application on http://localhost:5173/

After the installation, the package.json will look something like this. We can now see that the beta version is installed for both react and react-dom.

Implementation without using compiler

Before we jump to see the magic of the compiler, let's check how React normally works.

Consider the following code in App.jsx:

import { useState } from "react";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);
  const [names] = useState(["John", "Bob", "Alice", "Charlie"]);

  const sortedNames = (names) => {
    console.log(`Sorting names ${Math.random()} without compiler`);
    return names.toSorted();
  };

  const handleClick = () => {
    setCount((count) => count + 1);
    console.log("prevCount without compiler -> ", count);
  };

  return (
    <>
      <div>
        <button onClick={handleClick}>count is {count}</button>
        <div>
          {sortedNames(names).map((val) => {
            return <li key={val}>{val}</li>;
          })}
        </div>
      </div>
    </>
  );
}

export default App

Let's consider this straightforward example. Our application features two components. The first is a button connected to a state variable count, which increments with each click and includes a logger to track changes. The second component displays a list of names in a sorted order.

The sortedNames function, in an ideal scenario, should only run once, given that our state variable names doesn't change. However, ensuring this optimal behavior can be a bit of a dance without the right tools.

Let's see what happens when we run this application and click on the button multiple times.

As we can see, with every button click, even though the names state doesn't change, our sortedNames function gets called repeatedly. This results in unnecessary computations. In larger applications, such inefficiencies can lead to noticeable performance losses.

To optimise this, we will use the useMemo hook:

import { useState, useMemo } from "react";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);
  const [names] = useState(["John", "Bob", "Alice", "Charlie"]);

  const sortedNames = (names) => {
    console.log(`Sorting names with usememo ${Math.random()}`);
    return names.toSorted();
  };

  const handleClick = () => {
    setCount((count) => count + 1);
    console.log("prevCount with usememo -> ", count);
  };

  const memoizedSortedNames = useMemo(() => sortedNames(names), [names]);

  return (
    <>
      <div>
        <button onClick={handleClick}>count is {count}</button>
        <div>
          {memoizedSortedNames.map((val) => {
            return <li key={val}>{val}</li>;
          })}
        </div>
      </div>
    </>
  );
}

export default App;

Here we have memoized the sortedNames function and this will only run when the names state update.

Let's see how the application works now:

As we can see now, our sortedNames function was called only once on render and not on every button click.

Implementation using compiler

Now let's check how the compiler operates on a similar piece of code. But before that, we need to install the React compiler

Checking compatibility

Prior to installing the compiler, we can first check to see if our codebase is compatible:

npx react-compiler-healthcheck@latest

This script will:

  • Check how many components can be successfully optimized: higher is better

  • Check for <StrictMode> usage: having this enabled and followed means a higher chance that the Rules of React are followed

  • Check for incompatible library usage: known libraries that are incompatible with the compiler

On running the above command, the output should look something like this:

Need to install the following packages:
react-compiler-healthcheck@0.0.0-experimental-7054a14-20240601
Ok to proceed? (y) y
Successfully compiled 6 out of 7 components.
StrictMode usage found.
Found no usage of incompatible libraries.

Compiler installation using Babel

npm install babel-plugin-react-compiler

The compiler includes a Babel plugin which we can use in our build pipeline to run the compiler.

After installing, we need to add it to our Vite config. Please note that it’s critical that the compiler run first in the pipeline

vite.config.js should look something like this:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig(() => {
  return {
    plugins: [
      react({
        babel: {
          plugins: [["babel-plugin-react-compiler"]],
        },
      }),
    ],
  };
});

Usage

Now that our compiler is installed, let's check its working.

import { useState } from "react";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);
  const [names] = useState(["John", "Bob", "Alice", "Charlie"]);

  const sortedNames = (names) => {
    console.log(`Sorting names ${Math.random()} with compiler`);
    return names.toSorted();
  };

  const handleClick = () => {
    setCount((count) => count + 1);
    console.log("prevCount with compiler -> ", count);
  };

  return (
    <>
      <div>
        <button onClick={handleClick}>count is {count}</button>
        <div>
          {sortedNames(names).map((val) => {
            return <li key={val}>{val}</li>;
          })}
        </div>
      </div>
    </>
  );
}

export default App;

Here we have a similar example as our unoptimised case before. Let's see its working:

As we can see, without using any explicit memoization techniques, our code was optimized to run the sortedNames function only once. This efficiency is a testament to the power of the React Compiler.

Conclusion

As demonstrated in the examples above, with the introduction of the React Compiler, we can forgo the use of memoization techniques like useMemo or useCallback to optimize our applications. It's truly fascinating how the compiler works behind the scenes to achieve these results. However, diving into those intricate details will be a topic for another blog! In this post, we focus on a basic use case of the compiler. Even in its beta phase, such updates have the potential to dramatically transform our React development practices. Here's to hoping for a stable release soon!