React 19 and the React Compiler, previously known as React Forget, have been dominating the React discussion over the past month. We're all losing our minds (in a good way) over the possibility of never having to think about memoization in React very, very soon. Is it true, though? Should we start forgetting about memo
, useMemo
, and useCallback
in the coming months? And what actually changes when the React Compiler is released, and what should we learn and teach about React after that?
Let's take a look.
React 19 is NOT the React Compiler
Let's get the most important thing out of the way: memoization is not going anywhere soon, so don't unlearn it just yet. React 19 is not the React Compiler. The React team announced the Compiler in the same blog post where they announced the soon release of React 19, and everyone excitedly jumped to conclusions.
A tweet from a React team member, however, clarifies this confusion:
In React 19, we'll see a bunch of new features, but we'll have to wait a bit longer for the Compiler. It's not clear right now how long, but according to another tweet from a different React core team member, it might happen by the end of this year.
Personally, I'm skeptical of this timeline. If we look at the talk by the React team members that introduced the Compiler and its timeline, we're in the middle of the Compiler's journey:
The journed started in 2021, two years ago. Rolling out something as fundamental as this on a codebase as large as Meta is probably very complicated. So the jump from the middle of the timeline to the end might take another 2 years.
But who knows, maybe the React team will actually manage to release it this year. That would be good news. The current promise of the Compiler, mentioned in the video, is that we won't need to change any code for its adoption. It will Just Work ™️. If it's actually released by the end of the year, it's a very good indication that this is indeed the case, and the rest of us should be able to switch to it quickly and easily.
However, even if the Compiler is released this year, and it is indeed very easy to adopt with no downsides, it doesn't mean that we'll be able to forget about useCallback
and memo
right away. There will always be a "transition" period, where we first talk about the case of "if you already have the Compiler enabled", that slowly transitions to "in the rare case you haven't migrated to the Compiler yet" scenario.
The mental transition from class components to functional components with hooks took, I think, at least 3 years (starting from 2018) - when all the courses, docs, and blogs caught up, most people migrated to the React version with hooks, and we started talking about functional components and hooks as a default. And even today, 6 years later, there are still plenty of class components lurking around here and there.
If we apply a similar timeline to the Compiler, that would mean that we'd need to hold onto the knowledge of what memo
, useMemo
and useCallback
are for at least the next three years. Less, if you're lucky enough to work in a modern codebase that can migrate to the Compiler as soon as it's released. More, if you're a React teacher or work in a large codebase that is slower to migrate with plenty of legacy code.
What changes with the React Compiler
So what exactly is changing? The simplified answer is - everything will now be memoized. The React Compiler will be a Babel plugin that converts our typical React code into code where every hook dependency, props on components, and components themselves are memoized. Essentially, this code:
const Component = () => {
const onSubmit = () => {};
const onMount = () => {};
useEffect(() => {
onMount();
}, [onMount]);
return <Form onSubmit={onSubmit} />;
};
underneath will behave as if both onSubmit
and onMount
are wrapped in useCallback
and Form
is wrapped in React.memo
:
const FormMemo = React.memo(Form);
const Component = () => {
const onSubmit = useCallback(() => {}, []);
const onMount = useCallback(() => {}, []);
useEffect(() => {
onMount();
}, [onMount]);
return <FormMemo onSubmit={onSubmit} />;
};
The Compiler doesn't convert them into exactly that code, of course, it's much more complicated and advanced than this. But it's a good mental model to wrap our heads around it. If you're curious about the exact details, I recommend watching this video from the React core team members who introduced the Compiler. And if you're slightly fuzzy on why we'd use useCallback
and memo
here at all, I'd recommend watching the first six videos of the Advanced React series on YouTube. They cover everything about re-renders and memoization. Alternatively, if you're more into reading, then read everything here.
For the way we're teaching and learning React, this transition means a few things.
If parent re-renders, child re-renders
Currently, if a Parent component re-renders, every component rendered inside will re-render as well.
// if Parent re-renders
const Parent = () => {
// Child will also re-render
return <Child />;
};
A lot of people currently believe that the Child
component re-renders only if its props change. I like to call this The Big Re-renders myth. At the moment, this is not true. Props don't matter in the standard React behavior.
Funnily enough, it becomes true with the Compiler. Since everything is memoized under the hood, the current myth actually becomes the standard React behavior. In a few years, we're going to teach that a React component re-renders only if its state or props change, and whether the Parent re-rendered or not doesn't matter. Life is weird sometimes.
No more composition for performance
Currently, we have a few composition techniques like "moving state down" or "passing components as children" that can reduce re-renders. I usually recommend using them before messing around with useCallback
and memo
, since memoizing things in React properly is very, very hard.
For example, in this code:
const Component = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>
open dialog
</Button>
{isOpen && <ModalDialog />}
<VerySlowComponent />
</>
);
};
the VerySlowComponent
re-renders every time the dialog opens, causing the dialog to open with a delay. If we encapsulate the state that opens the dialog in a component like this:
const ButtonWithDialog = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>
open dialog
</Button>
{isOpen && <ModalDialog />}
</>
);
};
const Component = () => {
return (
<>
<ButtonWithDialog />
<VerySlowComponent />
</>
);
};
we essentially got rid of the unnecessary re-renders of the VerySlowComponent
without memoizing anything.
When the compiler comes to life, these patterns will become unnecessary for performance. We'll probably still use them for composition and separation of concerns purposes. But there won't be a natural re-renders power anymore that forces us to split components into smaller components. Our components might become larger with no negative consequences.
No more useMemo/useCallback everywhere
Naturally, all the useMemo
and useCallback
that sometimes plague our code will be gone. That part excites me the most. No more tracking props through multiple levels of components just to memoize one onSubmit
props callback. No more unreadable and undebuggable chains of useMemo
and useCallback
that all depend on each other and are impossible to understand. No more broken memoization just because children
are not memoized and no one noticed.
Diffing & reconciliation
We might need to change how we explain diffing & reconciliation in React. The current simplified explanation is that when we "render" a component like this <Child />
, we just create an Element of it. This element is an object of that shape:
{
"type": ...,
"props": ...,
// other react stuff
}
where "type" is either a string or a reference to a Component.
In this code:
const Parent = () => {
return <Child />;
};
when Parent
re-renders, its function is triggered, and <Child />
object is re-created. React performs a shallow comparison of that object before and after re-render, and if its reference changes, then it's an indication for React that it needs to do a full diffing on that sub-tree.
Currently, this is the reason why the <Child />
component always re-renders, even if it doesn't have any props. The result of <Child />
(which is a syntax sugar for React.createElement
function call) is an object that is always re-created, which means it can't pass the shallow comparison check.
With the React Compiler, the concepts of Elements, diffing, and reconciliation remain the same, so that's good. But it seems that <Child />
will now return a memoized object if its props haven't changed. So, actually, the end result of the Compiler is more of an equivalent of everything being wrapped in useMemo
, even the Elements.
const Parent = () => {
const child = useMemo(() => <Child />, []);
return child;
};
But this is just me making assumptions from the limited publicly available resources, so I might be slightly wrong here. In any case, it's just an implementation detail that doesn't really matter for our production code.
Everything else stays pretty much the same as it is right now. Creating components inside other components will still be a massive anti-pattern. We'll still use the "key" attribute to identify elements or reset the state. Context is still going to be a pain to deal with. And everything about data fetching or error handling is not even part of the conversation.
But anyway, I can't wait for the Compiler's release. It seems like a massive improvement to our React life. Even if I have to re-write half of my articles and re-do half of my YouTube videos because of it 😅
Originally published at https://www.developerway.com. The website has more articles like this 😉
Take a look at the Advanced React book to take your React knowledge to the next level.
Subscribe to the newsletter, connect on LinkedIn or follow on Twitter to get notified as soon as the next article comes out.