The React compiler is one of the biggest updates to the React framework in years. Last year, the React team released a beta version. Around that time, I wrote an article called “Exploring React’s Compiler: A Detailed Introduction”. It unpacked the core idea behind the compiler and showed how it could improve React development by automatically handling performance issues behind the scenes.
When the beta dropped, the React team encouraged developers to try it out, give feedback, and contribute. And many did. A standout example is Sanity Studio. They used the compiler internally and shipped libraries like react-rx
and @sanity/ui
that are optimized for it.
Thanks to contributions like these, the React team officially released the first Release Candidate (RC) on April 21. According to the release blog, RC is meant to be stable and production-ready.
In this article, we’ll go over what’s new in RC and what it means for you as a React developer.
We’ll get into the nitty gritty below, but here’s a quick summary of what the RC promises:
Now let’s take a step back and dive a little deeper into the thought process behind the React Compiler.
To recap, the React Compiler is a build-time tool that automatically optimizes your React apps through memoization. Simply put, the compiler analyzes your React code during the build process and strategically inserts memoization wherever it sees potential performance gains. This eliminates the tedious and often confusing process of manual optimization using tools like React.memo()
, useMemo()
, and useCallback()
.
The release of RC is a big milestone. As I mentioned in the previous article, the compiler has been in development for a while. It was originally called “React Forget,” but it’s come a long way since then.
The beta version gave us an early look and drew a lot of useful feedback. Case studies from teams like Sanity Studio and Wakelet helped shape the direction. Now with the Release Candidate, the compiler is moving toward full stability. The React team believes it’s ready for real-world use.
The typical progression from beta to release candidate is solidifying the existing functionality, improving stability, and making the developer experience smoother in preparation for the final stable release. RC does all that, but it also includes a few new updates. Here’s what stands out:
In the beta release, the compiler supported several build tools like Vite, Babel, and Rsbuild, but not SWC, a Rust-based tool used in frameworks like Next.js and Parcel. With RC update, the compiler now supports SWC as an SWC plugin.
This integration is still a work in progress, but it’s promising. It brings better Next.js build performance by using a custom optimization in SWC that only applies the compiler to relevant files, i.e., files containing JSX or React Hooks, instead of compiling everything. This helps keep build times fast and minimize performance costs.
The process for enabling the compiler in Next.js remains the same as outlined in the previous article, so you can refer to it if you’d like to use it in your project. However, since SWC support is still experimental, things may not work as expected. The React team recommends using Next.js v15.3.1 and above for optimal build performance.
Another notable update is the migration of eslint-plugin-react-compiler
into eslint-plugin-react-hooks
. As mentioned before, the compiler relies on strict adherence to React’s rules. That’s what the dedicated compiler plugin helped enforce.
With RC, you no longer need that separate package. It’s now part of the main ESLint plugin for React. If you’ve been using eslint-plugin-react-compiler
, you can uninstall it and switch to eslint-plugin-react-hooks
:
npm install --save-dev [email protected]
Then enable the compiler rule with your Eslint config file (for flat config or legacy config):
// eslint.config.js import * as reactHooks from 'eslint-plugin-react-hooks'; export default [ // Flat Config (eslint 9+) reactHooks.configs.recommended, // Legacy Config reactHooks.configs['recommended-latest'], { rules: { 'react-hooks/react-compiler': 'error', }, }, ];
Unlike the dedicated ESLint plugin for the compiler, the new rule in eslint-plugin-react-hooks
doesn’t require the compiler to be installed. So there’s no risk in upgrading to it, even if you haven’t adopted the compiler yet.
N.B. To enable the rule, add 'react-hooks/react-compiler': 'error'
to your ESLint config, as shown in the example above.
In the beta version, the compiler was only compatible with React 19, and the fastest way to use it was within a Next.js project. However, with the RC release, the compiler now supports backward compatibility.
It still works best with React 19, but if you’re not ready to upgrade, you can install a separate react-compiler-runtime
package to run compiled code on older versions (just not earlier than React 17).
For Vite users, the setup is mostly the same, using the babel-plugin-react-compiler
package:
npm install --save-dev --save-exact babel-plugin-react-compiler@rc
And add it as a Babel plugin:
=// babel.config.js const ReactCompilerConfig = { /* ... */ }; module.exports = function () { return { plugins: [ ['babel-plugin-react-compiler', ReactCompilerConfig], // must run first! // ... ], }; }; // vite.config.js const ReactCompilerConfig = { /* ... */ }; export default defineConfig(() => { return { plugins: [ react({ babel: { plugins: [ ["babel-plugin-react-compiler", ReactCompilerConfig], ], }, }), ], // ... }; });
But if you’re using an older React version, like 18, the process includes one extra step, as mentioned earlier. You’ll need to install the react-compiler-runtime
package:
npm install react-compiler-runtime@rc
After that, update your babel.config.js
or vite.config.js
file to explicitly target the React version you’re using:
const ReactCompilerConfig = { target: '18' // '17' | '18' | '19' }; module.exports = function () { return { plugins: [ ['babel-plugin-react-compiler', ReactCompilerConfig], ], }; };
The React team is also collaborating with the oxc team to eventually add native support for the compiler. Once Rolldown, a Rust-based bundler for JavaScript and TypeScript, is released and supported in Vite, developers should be able to integrate the compiler without relying on Babel.
The compiler’s ability to automatically track dependencies is already a game-changer, as it reduces the need for manual specifications in optimization Hooks like useEffect
, useMemo
, and useCallback
. RC builds on this by improving how it handles more complex JavaScript patterns, like optional chaining and array indices.
?.
)Previously, when dealing with nested objects and potential null
or undefined
values, you might have written code like this with manual dependency arrays:
import React, { useState, useEffect } from 'react'; function UserProfile({ user }) { const [displayName, setDisplayName] = useState(''); useEffect(() => { if (user && user.profile && user.profile.name) { setDisplayName(user.profile.name); } else { setDisplayName('Guest'); } }, [user && user.profile && user.profile.name]); // ... }
With the RC, the compiler can now intelligently track the dependency on the nested property even with the optional chaining:
import React, { useState, useEffect } from 'react'; function UserProfile({ user }) { const [displayName, setDisplayName] = useState(''); useEffect(() => { setDisplayName(user?.profile?.name || 'Guest'); }, [user?.profile?.name]); // Compiler can understand this dependency now // ... }
The compiler now understands expressions like user?.profile?.name
. It knows to re-run the effect if user.profile.name
changes, even if user
or user.profile
starts as null
or undefined
and then gets a value later. There’s no need to manually track each piece.
Similarly, when an effect or memoized value depends on a specific element within an array, you previously had to explicitly include that element in the dependency array:
import React, { useState, useEffect } from 'react'; function ItemList({ items }) { const [firstItemName, setFirstItemName] = useState(''); useEffect(() => { if (items && items[0]) { setFirstItemName(items[0].name); } else { setFirstItemName(''); } }, [items && items[0] && items[0].name]); // Manual dependency on the first item's name // ... }
Now, the compiler can understand the dependency on a specific array index:
import React, { useState, useEffect } from 'react'; function ItemList({ items }) { const [firstItemName, setFirstItemName] = useState(''); useEffect(() => { setFirstItemName(items?.[0]?.name || ''); }, [items?.[0]?.name]); // Compiler understands the dependency on the first item's name // ... }
Here, the compiler will correctly identify that the useEffect
depends on items[0].name
, it knows to re-run if either items
changes or the name
of the first item changes. This takes a lot of the guesswork and boilerplate out of writing stable, performant Hooks.
As a frontend developer, you might be wondering how this update impacts your day-to-day work, both the good and the bad.
The first thing you need to know is that RC isn’t just a performance update optimization under the hood; it has a substantial impact on how frontend developers write, maintain, and even think about their React applications.
One of the immediate impacts of this update is the potential for significantly reduced boilerplate code. With improved dependency inference, the compiler’s ability to track dependencies means developers can write more concise and readable code.
Take this typical pre-compiler scenario:
import React, { useState, useEffect, useMemo } from 'react'; function ProductDetails({ product }) { const discountedPrice = useMemo(() => { return product && product.price * (1 - (product.discount || 0)); }, [product && product.price, product && product.discount]); const [formattedPrice, setFormattedPrice] = useState(''); useEffect(() => { if (discountedPrice !== undefined) { setFormattedPrice(`$${discountedPrice.toFixed(2)}`); } }, [discountedPrice]); return ( <div> {product && <h1>{product.name}</h1>} <p>Price: {product && `$${product.price}`}</p> {discountedPrice !== undefined && <p>Discounted Price: {formattedPrice}</p>} </div> ); }
If product
and its properties rarely change, the compiler can now automatically memoize the component and optimize effects based on how the data actually flows. That means you can safely skip the manual use of useMemo
or detailed dependency arrays, if the compiler determines it’s safe to do so:
import React, { useState, useEffect } from 'react'; function ProductDetails({ product }) { // Compiler can likely optimize this component based on data flow const discountedPrice = product?.price * (1 - (product?.discount || 0)); const [formattedPrice, setFormattedPrice] = useState(''); useEffect(() => { setFormattedPrice(`$${discountedPrice?.toFixed(2)}`); }, [discountedPrice]); // Compiler understands dependency on discountedPrice return ( <div> <h1>{product?.name}</h1> <p>Price: ${product?.price}</p> {discountedPrice !== undefined && <p>Discounted Price: {formattedPrice}</p>} </div> ); }
The result? Cleaner components that are easier to read and maintain.
The compiler as a whole will eventually change how we think about performance. Instead of proactively identifying bottlenecks and manually memoizing components and values, you can just focus on writing clear, functional components that follow React’s best practices.
That doesn’t mean you can forget about performance entirely. But it does give you a more “performance-by-default” foundation, something other frameworks often boast of. As long as you follow the rules of React, the compiler takes care of the rest.
At first glance, the compiler seems like it should “just work,” and in most cases, it does. But someone on X (formerly Twitter) raised a fair point: how do you know what is being memorized?:
That caught my attention. The whole point of the compiler is that you shouldn’t have to worry about that. But this user went on to describe edge cases where automatic memoization might not apply. So, how do you tell when it’s working and when it’s not?
Although I covered how to do this in the previous article, I guess it could also be considered a learning curve. Not a steep one, but enough that every developer will need to build some understanding of how the compiler works, what its limitations are, and how to reason about it when debugging performance issues.
One potential drawback of the compiler is the slow adoption rate among popular libraries. A good example is React Hook Form, which can run into issues when the compiler is enabled. This was pointed out by a Reddit user who’s been using the compiler in production on some projects.
It’s not too surprising, since many libraries occasionally bend React’s rules, so some won’t work with the compiler right away. And since the compiler isn’t fully stable yet, most libraries haven’t had time to officially adopt it.
For developers considering the Compiler at this time, this is an important factor to keep in mind.
The release of the RC update is a big step forward, not just for the compiler itself, but for how we build and optimize React apps. With smarter dependency inference, simpler ESLint integration, and early support for tools like SWC, it’s clear the React team is serious about making performance a built-in feature, not an afterthought.
For developers, this means less time fine-tuning and more time focusing on building great user experiences.
That said, the compiler is still evolving. If you get a chance to try it in your projects, your feedback will go a long way in shaping where it goes next.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe Model Context Protocol (MCP) provides a standardized way for AI models to access contextual information from diverse data sources.
Explore RBAC, ABAC, ACL, and PBAC access control methods and discover which one is ideal for your frontend use case.
There’s been major controversy surrounding Next.js’s openness. Discover how OpenNext is addressing the bubbling issue of Next.js portability.
By using SRP, developers can produce code that is easier to debug, test, and extend, making it clearer, more maintainable, and scalable.