Blog Hero Background

React Will Be Compiled

In some ways it always was.
But now you can forget about memoization

User Avatar
Brad WestfallSay hi on Twitter
February 16, 2024

Upcoming Workshops

Yesterday, the React team made this blog post announcing what they've been working on for React. Andrew Clark from the React team gives us a nice breakdown of the changes:

[Correction] I previously stated that it would be v19 that would be compiled. The React team's announcement talked about compiled React and I assumed (along with others) that this meant v19. It appears v19 will have lots of features mentioned in their post but being compiled will probably be the following version (as Andrew indicates, probably by the end of this year, 2024).

Whatever the version, I hope this post helps anyone who feels confused about what it means for React to become "compiled". I'll try to show examples and historical context for how we got to this point because it has been a heavily discussed topic and is sometimes hard to follow along, especially if you haven't seen the whole story of React play out.

Compiled React will fix the main issues of hooks

As we continue, keep these React principals in mind that will not change when we Compile React:

  1. React state is immutable
  2. UI is a function of state
  3. Re-render when state changes to produce new UI

Aside from version numbers, I think of React as having three distinct eras.

  • The class components era (no primitive for abstraction)
  • The Hooks era (we need to memoize)
  • The compiled era (auto-memoization)

We are about to enter the compiled era, but how did we get here?

For those of us who created projects with class components, we remember the problems that classes gave us when we wanted to abstract and re-use our code. React was lacking a "primitive" for re-using code so the community invented patterns like Hoc's and Render Props which were less than ideal. It turns out the problem with making a primitive was that classes themselves didn't give us the level of composition we needed. So the React team started looking away from classes and into functional composition.

At the time, functional components existed but we called them Stateless Functional Components because they couldn't have state or other lifecycle stuff that classes had. The React team saw functional components as a way to give us the primitive we needed. If only they could figure out a way to let functional components "hook" into the lifecycles of React 😉

Yes, that's where the term "hooks" comes from.

When hooks were announced in 2018 I was at the conference. I remember when Ryan Florence went up to talk right after the announcement and did a "render-props to hooks" refactor in front of everyone. We were floored. Hooks, and specifically custom hooks, were going to be the primitive we were missing.

What we didn't realize at the time was that co-mingling all of our code into one function might provide us with composition, but would come as a tradeoff because now we'd have to memoize. We didn't realize classes were innately shielding us from memoization, given the nature of re-rending.

In class components, the render method isolates its code from the other lifecycle methods which in turn means a re-render won't adversely effect code that's not apart of the render phase. This was probably less of a design decision, and more of a characteristic of that's how classes work. 🧐 This seems almost silly to bring up at all, but it plays a role in the evolutions that would come next.

Memoized React

Class components were terrible, to be honest. I remember when we changed our two-day workshop curriculum to hooks and half of our topics just evaporated because class components brought so much complexity into apps that we didn't have to teach anymore.

If we made a class-component with a method to handle on-submit, the method would never need to be "memoized". Let's see what happens when we do something similar with functional components:

function App() {
const [state, setState] = useState()
function onSubmit() {
// Submit logic
return <form onSubmit={onSubmit}></form>

Maybe you didn't notice it right away, but this function is going to re-create itself every time there is a re-render, meaning it will be a whole new function in memory. It's usually not a problem that functions re-create themselves and in this example it's not creating any issues for us. It is worth noting though that this would not happen in classes because it would have been a method, separate from the render phase.

It's also worth noting that the general idea of things needing to re-create themselves in JavaScript is not specific to React. I can show you my jQuery code from 2008 that would re-create functions and objects also. I'm just joking, I have no idea where my 2008 code is.

Now let's re-factor the code a little:

function App() {
const [state, setState] = useState()
function onSubmit() {
// Submit logic
return <Form onSubmit={onSubmit} />
const Form = ({ onSubmit }) {
// ...

It's still not a problem that onSubmit will be a new function on every render.

The re-render of App will cause a re-render of Form in this case. Some will say that a component will get a re-render when if its props change. That's not true. The Form will get a re-render when App gets a re-render, regardless of props. For now, it simply doesn't matter if the onSubmit prop is changing.

Now let's say we have some reason to prevent Form from getting re-rendered when App gets a re-render. This example is over simplistic but let's say we memoize Form:

// Now Form will only re-render if it's specific props change. Not every
// time App re-renders
const Form = React.memo(({ onSubmit }) {
// ...

Now we have a problem.

React heavily relies on strict equality checks to know if a variable changed which is a fancy way of saying they use === and to compare the old to the new. When you compare JavaScript primitives (like strings) to each other with ===, JS will compare them by values (you already knew that). But when JS compares arrays, objects, or function to each other, the use of === is comparing their identity, in other words their memory allocation. This is why {} === {} is false in JavaScript because those are two different object identities in memory.

Doing Form = React.memo(fn) is like saying:

Hey React, we only want to re-render Form if its props do really change according to an identity check.

This creates a problem because onSubmit changes every time App gets re-rendered. This will lead to Form always getting a re-render which means the memoization does nothing for us. It's meaningless overhead for React at this point.

Now we have to go back and ensure onSubmit doesn't change its identity when App re-renders:

function App() {
const [state, setState] = useState()
const onSubmit = useCallback(() => {
// Submit logic
}, [])
return <Form onSubmit={onSubmit} />

We use useCallback to stabilize the function so its identity doesn't change. In a way, it's a type of memoization. In overly simplistic terms, memoization means to "remember" or "cache" the response of a function.

It's like we're saying:

Hey React, remember the identity of this function I'm passing to useCallback. When we get re-renders, I'm giving you a new function every time but forget that, give me the original function's identity from the first time I called you.

Memoizing the onSubmit function is not usually necessary, but it became necessary when Form got memoized and received onSubmit as a prop. At React Training, this is what we refer to as "implementation bleed".

The problem doesn't stop there. Let's add more code:

function App() {
const [state, setState] = useState()
const settings = {}
const onSubmit = useCallback(() => {
const x = settings.x
// ...
}, [])
// ...

The settings object re-creates itself on every render of App. This is not an issue by itself, but if you know React well, you know that the linter will ask you to put settings in the dependency array of useCallback in this case:

const settings = {}
const onSubmit = useCallback(() => {
const x = settings.x
// ...
}, [settings])

If we do that, it's like we're saying:

We want onSubmit to be stable and not change on every render. But we do want useCallback to re-create onSubmit if any of the things in this dependency array change.

You might ask yourself, "why would I want onSubmit to change?"

I would agree with you, it probably doesn't need to change but there are many situations in React where things like useCallback and useMemo do need to re-memoize and create a new identity for their return value when their dependency array changes. The linter just doesn't know that we never want onSubmit to be different in this case.

Keep in mind the linter is almost always right, but I hand-picked this example to show how we might not want what the linter wants.

If we listen to the linter and put settings in the dependency array, here's what will happen:

  1. When App re-renders...
  2. settings becomes a new object that is not === to the one in the previous render.
  3. The dependency array sees settings as different according to === even though it's values haven't changed.
  4. The change in the dep array means useCallback returns a new identity for onSubmit
  5. The Form gets re-rendered because onSubmit changes.

In short, Form's memoization is useless. It will always get re-rendered when App gets re-rendered. So now we have more implementation bleed because we need to memoize settings with useMemo just so we can keep the memoization of onSubmit in tact.

Let's take a step back to that question:

Why would I want onSubmit to change? Couldn't we just disable the linter in that case?

Sure, in this case I think we can leave settings out of the dependency array or we can just memoize it which is what I would probably do. Or we could even argue that we didn't need the memoized form in the first place which would have prevented this mess. That's not the point, it's just an example. The point is that memoizing in React often leads to a cascade of implementation bleed.

The topic of dependency arrays and why the linter wants you to put things in them goes way beyond the scope of this post. I can probably talk about this topic for hours because it is vast with many nuances. The truth is, the linter is usually right and it has good intentions. The problem is that a LOT of React developers don't understand its reasoning and think the linter is just a small suggestion. In my experience, when you ignore the linter, you'll probably get bugs.

Here's a perfect example: Some years ago I was talking to someone on Twitter that said they never put functions in their useEffect dependency array because it sometimes creates an infinite loop. I said something like "why don't you use useCallback on those functions, that will prevent the loop. The problem is the fn is changing too often".

They said "What's useCallback?"

This is common that people don't understand memoization or React well enough and then get frustrated with the linter.

Dependant on memoization

If you've worked in React enough, you know it can be a pain to deal with dependency arrays. The linter might tell you to put stuff in the array and you don't like the outcome (like a loop). It's easy to get mad at the linter but the linter was right. Not because React "wants" an infinite loop of course, but you needed to also memoize something now.

Dependency arrays are a way to deal with the fact that all our code is co-located into a functional component that re-renders and we want to monitor changes to variables over time. Sometimes we end up putting objects, arrays, and functions in the dependency array, so make sure you stabilize them with memoization. The way I explain what React means by "stable" is "a variable that doesn't change unless you want it to".

Let's demonstrate this with code:

function App() {
const [misc, setMisc] = useState()
const [darkMode, setDarkMode] = useState(false)
const options = { darkMode }
return <User options={options} />
function User({ options }) {
useEffect(() => {
// get user
}, [options])
// ...

We can see that when the misc state in App changes, the cascading consequence is that options will change and therefore the useEffect will run again even though the effect has nothing to do with the misc state. So you better wrap that options variable in a useMemo. When you do, the linter will rightfully ask you to put darkMode in the dependency array:

const [darkMode, setDarkMode] = useState(false)
const options = useMemo(() => {
return { darkMode }
}, [darkMode])

By doing so, we're saying:

We want options to be stable, until dark mode changes. Then re-stabilize it into a new identity. But don't do anything when misc state changes because it's not in our array (we don't depend on it).

Okay, I hope you're get the point that React depends on memoization. You have to write it yourself. You better get it correct or you'll have bugs and performance issues.

We've always compiled React

Depending on your definition of the term, you could argue that React has always had a compile step (JSX). To me, it seems like it's a loose term in JavaScript that basically means the code you write is different than the code that runs in the browser.

My first experience doing React was in 2015. Babel and React were still fairly new to most devs. In a way, their popularity grew in tandem with each other. React is famously known for compiling JSX to function calls. So I guess React is technically compiled but I've always felt like it was a small syntax sugar and the semantics of one JSX element becoming a very predictable function means to me that it's a fairly "light" amount of compiling.

Today, we also compile TypeScript into JavaScript which to me is funny because in this case it just means all the TS that we write evaporates when we save and the code that's left is the JavaScript. But I guess it still meets my definition of "is what you write, what you get".

Compiling is a spectrum

To me, it feels like "compiled frameworks" sit on a spectrum where some are compiled a little and some are a lot:

A spectrum of compiling JavaScript

React feels like it sits on the "not very much" side compared to some other modern JS frameworks. For me, the "what you see is what you get" rule will determine where you are on this spectrum. JSX means that React is somewhat compiled, but the other code I write is not compiled by React at all.

By contrast, Svelte is so heavily compiled that its creator has described it as not even being JavaScript anymore. That Svelte is really more of a programming language because the semantics of what you write are so far from the semantics of what you get when it turns into JavaScript.

I'm not trying to make this a comparison post, or to say one way is better than the other, or that compiling is good or bad. I'm simply saying it feels like a spectrum where different JavaScript frameworks either compile less, or compile more, or compile to the point where they're not even really JS anymore.

The announcement from the React team is that React is going to be more compiled than it was before. Will it be more than some of the others? I'm not sure. It doesn't really matter where it ends up on this spectrum to me. What matters more is why its compiling. The answer is for different reasons than the others.

Compiling for Auto Memoization

React is not going away from immutability and towards observability. It will still have identity checks and dependency arrays. So the fact that its compiled now doesn't make React feel similar to the others. It's going to be compiled so we can have auto-memoization. React is the same as it's been for years but without the downsides of manual memoization which was one of the main issues with hooks and functional components.

Personally, I'm accustomed to my logic above the JSX being left untouched. This change will mostly be un-learning how to think in terms of manual memoization. I'll have to trust the compiler to make good decisions for me and I'm still uncertain as to how much of this I'll have to guide the compiler to vs "It Just Works™". I'm optimistic and interested to play with it.

In summary, its worth noting that this idea didn't just spring up out of nowhere. We've been talking about this as a possibility in React for three years since Xuan Huang introduced the idea at React Conf 2021. There's also been times when it was the hot topic on Twitter in the React circles for a few years now.

My hope is that if you haven't been aware of these conversations, that this post provides fair examples and context as to how we got to this point. Thanks for reading!

Instead of having comments here in our blog, we've tweeted about this post so you can comment there if you wish.

View on Twitter

Photo by Nick Hillier on Unsplash

Subscribe for updates on public workshops and blog posts

Don't worry, we don't send emails too often.

i love react
© 2024