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 followedCheck 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!