Skip to solution
hardFrontend

React Compiler pitfalls — when does auto-memo bail out?

224 views
01

Understand the problem

Question presented to candidate: "You enabled the React Compiler but the profiler shows components still re-rendering. What is going on?"

What a strong answer should cover:

  • The compiler bails out per component when it cannot prove the transformation is safe. That component is left exactly as written; the rest of the file is still compiled.
  • The failure mode is silence — you do not get an error, you get an unoptimised component. That is deliberate: it will never miscompile, only decline.
  • The common bail-out causes: mutating props or state, reading or writing a ref during render, other impure render behaviour, and code the analysis cannot follow.
  • The ESLint rule is the real adoption tool — it reports the violations, which is how you find out what was skipped.
  • Escape hatches: the "use no memo" directive to exclude a component deliberately, and opt-in mode to compile only annotated files.
  • Memoisation is a performance hint, not a semantic guarantee — code must still be correct if a value is recomputed.
  • It does not fix effect dependency arrays, refactor your state placement, or virtualise a list.
  • Existing manual useMemo and useCallback keep working; removal is a follow-up cleanup, not part of adoption.

Clarifying questions expected:

  • "Does the codebase pass the React Compiler ESLint rule?" — that answers the question directly.
  • "Is the compiler actually running on this file, or is it in opt-in mode?"

Code / implementation expected: Optional. Showing a component that would bail out, and its fixed version, is the useful form.

react-compilerperformance
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for senior React interviews — assumes the React Compiler basics. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. A note on verification: the React Compiler is **not installed

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A component that would bail out, and the same one made compilable
Run Playground
import { useState, useRef } from "react";

// ❌ WOULD BAIL OUT: mutates its props. The compiler cannot memoise around a
//    component that modifies its inputs, because the whole model assumes props
//    are read-only. In development React freezes props, so this also throws.
function MutatesProps({ config }) {
  const [report, setReport] = useState("not tried");
  const attempt = () => {
    try {
      config.touched = true;               // <- the bail-out cause
      setReport("mutation succeeded (and the compiler would skip this component)");
    } catch (e) {
      setReport("threw: " + e.message.slice(0, 60));
    }
  };
  return <Row label="❌ mutates props" onClick={attempt} note={report} />;
}

// ❌ WOULD BAIL OUT: reads a ref DURING RENDER. Render is then no longer a pure
//    function of props and state, so the output cannot be safely cached.
function ReadsRefInRender() {
  const counter = useRef(0);
  counter.current++;                        // <- impure: a write during render
  const impure = counter.current;            // <- and a read of it
  return <Row label="❌ ref during render" note={"render count read as " + impure} />;
}

// ✅ COMPILABLE: pure render, props untouched, ref only used in a handler.
function Compilable({ config }) {
  const [count, setCount] = useState(0);
  const clicks = useRef(0);
  // Derived during render from props and state only — no mutation, no ref read.
  const label = config.prefix + ": " + count;
  const bump = () => {
    clicks.current++;                        // refs in handlers are fine
    setCount((c) => c + 1);                  // immutable update
  };
  return <Row label="✅ pure" onClick={bump} note={label} />;
}

function Row({ label, note, onClick }) {
  return (
    <p style={{ margin: "6px 0", fontSize: 14 }}>
      <code style={{ display: "inline-block", minWidth: 180 }}>{label}</code>
      {onClick && <button onClick={onClick} style={{ marginRight: 8 }}>run</button>}
      <span style={{ color: "#555" }}>{note}</span>
    </p>
  );
}

export default function App() {
  const [config] = useState({ prefix: "count", touched: false });

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 560 }}>
      <MutatesProps config={config} />
      <ReadsRefInRender />
      <Compilable config={config} />

      <p style={{ color: "#666", fontSize: 13, marginTop: 12 }}>
        This playground has no React Compiler configured, so nothing here is
        actually compiled — the point is which components it <em>would</em>
        skip. Both failing cases violate React rules independently of the
        compiler, which is why the ESLint rule flags them either way.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 113 of 119 decoded in the React.js track. One more won't hurt.

Back to track