Frontend Development7 min read

The React Compiler is stable. Here's what I stopped writing.

By Marin Cholakov08/05/2026
ReactReact CompilerPerformanceHooks

For most of the time I've written React, a meaningful share of every component was insurance. useMemo around a derived value in case it was expensive. useCallback around a handler in case a child was memoised. React.memo around the child in case the parent re-rendered too often.

Almost none of it was measured. It was ritual.

The React Compiler removes the need for most of it. It reads your components at build time and inserts the memoisation itself, based on what actually depends on what. The interesting part is not the performance — it's how much smaller the code gets.

What goes away

Here's a component in the shape I used to write:

function ProductList({ products, currency, onSelect }) {
  const sorted = useMemo(
    () => [...products].sort((a, b) => a.price - b.price),
    [products]
  );

  const format = useCallback(
    (value) =>
      new Intl.NumberFormat("bg-BG", { style: "currency", currency }).format(value),
    [currency]
  );

  const handleSelect = useCallback((id) => onSelect(id), [onSelect]);

  return sorted.map((p) => (
    <ProductCard key={p.id} product={p} format={format} onSelect={handleSelect} />
  ));
}

And the same component with the compiler doing the work:

function ProductList({ products, currency, onSelect }) {
  const sorted = [...products].sort((a, b) => a.price - b.price);
  const format = (value) =>
    new Intl.NumberFormat("bg-BG", { style: "currency", currency }).format(value);

  return sorted.map((p) => (
    <ProductCard key={p.id} product={p} format={format} onSelect={onSelect} />
  ));
}

The second version is what the component always meant. The first version is what I had to write so React would behave.

What it does not do

The compiler memoises. It does not fix architecture.

It will not save you from fetching in a component that renders in a loop. It will not collapse three context providers that each re-render half the tree. It will not make a 4,000-row table virtualised. Those were always the problems worth measuring, and they still are — the difference is that they are no longer hidden underneath a layer of hand-written useMemo that made everything look optimised.

It also cannot memoise what it cannot prove is safe. If a component mutates something during render, or reads from a mutable ref in a way the compiler can't reason about, it bails out on that component and leaves it alone. This is the right call, and it is worth knowing about: silence is not confirmation. The ESLint plugin tells you where it bailed, and those spots are usually worth a second look on their own merit.

How I'd approach adopting it

Turn it on for one route, not the whole app. Read the bail-out list. Then delete memoisation in that route only, and only where you can see the compiler covering it.

Deleting useMemo everywhere in one commit is the version of this that goes badly. Not because the compiler is unreliable, but because you lose the ability to tell which change caused a regression. On a codebase of any size, the boring migration is the one that finishes.

The part I did not expect

The real win has been in code review. Reviewing a component used to mean checking whether the dependency arrays were right — a mechanical task that caught real bugs but consumed the attention you wanted to spend on whether the component was doing the right thing at all.

That whole category of review comment is gone. What's left is the logic. That turns out to be worth more than the frames.