DEV Community

Cover image for Goodbye useCallback and useMemo: How React Compiler Takes Over
Antonio Moruno Gracia
Antonio Moruno Gracia

Posted on

Goodbye useCallback and useMemo: How React Compiler Takes Over

For React developers, optimizing component performance often means managing a set of tools like useCallback, useMemo, and memo to prevent unnecessary re-renders. These hooks allow developers to memoize values and functions, keeping their React apps efficient.

However, most of the times they also clutter the code and require frequent tuning, making their usage more of a headache rather than a helpful tool for clean, maintainable code.

React Compiler changes this paradigm by automatically handling these optimizations, allowing developers to focus on writing clean, maintainable code that follows React's best practices without being distracted by performance concerns.

By analyzing your component code and applying performance enhancements behind the scenes, React Compiler makes it possible to write React code that’s both high-quality and efficient, without manually configuring memoization.

With this, we’re looking at a future where developers can concentrate on building components the right way—using React principles—while React Compiler takes care of performance and optimization.

The Current State of Memoization in React

To understand how React Compiler changes things, let’s review how manual memoization works in React today.

useCallback

The useCallback hook allows us to memoize functions, ensuring they’re only recreated when their dependencies change. This is particularly useful when functions are passed as props to child components, as it prevents those components from re-rendering unnecessarily.

import { useCallback } from 'react';

const MyComponent = ({ onClick }) => {
  const handleClick = useCallback(() => {
    // handle click event
  }, [/* dependencies */]);

  return <button onClick={handleClick}>Click me</button>;
}
Enter fullscreen mode Exit fullscreen mode

useMemo

useMemo works similarly but is used to memoize values rather than functions. It’s commonly used for heavy computations or data transformations that don’t need to be recalculated every render.

import { useMemo } from 'react';

const MyComponent = ({ data }) => {
  const processedData = useMemo(() => {
    return data.map(item => process(item));
  }, [data]);

  return <ChildComponent data={processedData} />;
}
Enter fullscreen mode Exit fullscreen mode

memo

Finally, the memo function can be used to memoize entire components. If the props passed to the component haven’t changed, memo skips re-rendering the component.

When could this be useful? Well, imagine this situation:

export const Component = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <Title />
      <Counter count={count1} setCount={setCount1} />
      <Counter count={count2} setCount={setCount2} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Anytime count1 or count2 changes, the whole component and its children are rerendered (even if those children doesn't use the state). Wrapping those components with memo will avoid those unnecessary rerenders.

const Title = memo(function Title({ value }) {
  ...
});

const Counter = memo(function Counter({ value }) {
  ...
});
Enter fullscreen mode Exit fullscreen mode

Now, the only children components that will rerender are those whose props have changed. If count1 changes, only the first <Counter/> will rerender. If count2 changes, only the second <Counter/> will rerender.

How React Compiler Changes the Game

React Compiler seeks to make these optimizations for us, eliminating the need for manually configuring useCallback, useMemo, or memo. By analyzing each component and its dependencies, React Compiler can automatically determine where and when to apply memoization. This means no more worrying about remembering which hooks to use or fine-tuning dependencies lists—React Compiler handles it all.

To highlight this, I will make use of this react compiled code visualizer. Let's take a look at an example.

Current transpiler

Simple react example transpiled

We can see that the transpiler simply calls the __jsx function (which works similarly to React.createElement) to build the virtual DOM.

React compiler

Simple react example compiled

Let’s break this code down:

  1. React introduces the c hook and uses it within the component. This creates an array of cacheable slots (called $ in this case), with as many slots as the compiler deems necessary (two in this case).

  2. The first slot is always reserved for the component's hash—a unique identifier for each component, calculated based on its contents, including subcomponents, hooks, and other properties.

  3. The remaining slots are reserved for the memoized version of the JSX (in this case, there is one slot, the second one).

  4. We check if these slots have stored data. If they do, the memoized component is stored in the slot for reuse.

  5. When a rerender occurs and this function is called again, the memoized component is retrieved from the $ array without needing to call the __jsx function again, preventing unnecessary rerendering.

This is a simple example, but it illustrates the concept well. Here are some other key points about the React compiler:

  • It handles nested components, hooks, and other complex structures seamlessly.
  • It identifies constants in the component that won’t change, avoiding redundant creation by directly using their values.
  • If a component breaks any of the Rules of React, the compiler switches to a safe mode. In this mode, the component isn’t memoized, which prevents potential bugs or unexpected behavior.
  • And more.

Final thoughts

React Compiler promises to revolutionize how we approach performance optimization in React. By eliminating the need for manual memoization hooks, it simplifies code and allows developers to build applications without getting bogged down in performance tweaks. As React Compiler becomes more integrated, it will free us from useCallback, useMemo, and memo, making React development more intuitive and accessible than ever, letting us focus on what really matters: building great apps.

That said, React Compiler is still in development and is not yet recommended for production use. However, there are some linting rules available, such as eslint-plugin-react-compiler, that prepare us for when the compiler becomes production-ready by surfacing problematic React code detected by the compiler.

This post is inspired in this youtube video. Don't hesitate to check it out in case you want to learn more about React compiler!

Top comments (0)