The React Compiler only produces correct output for code that follows the Rules of React. Which properties does it rely on, and what happens when a component breaks them?
answer
- memoization is a bet on purity
- who owns the object you are mutating
- conservative skip vs silent staleness
- the lint rule is the precondition
- the bug predates the compiler
basics
~20 sThe React Compiler assumes rendering is pure: same inputs produce the same output, and props, state and rendered values are never mutated during render. When it can detect a violation it skips that component and leaves it uncompiled; undetectable violations can cache a stale value and show stale UI.
solid answer
~50 sAuto-memoization is only safe if re-running a component is optional, so the compiler assumes the Rules of React hold: render is pure, it does not mutate props, state, or values it has already handed to JSX, it does not read or write refs during render, and hooks are called unconditionally. Where it can statically prove a violation, it bails out — it simply does not compile that function, so the code keeps working, just unoptimised. The dangerous case is a violation it cannot see, typically mutating an object that came from a prop, a module-level cache, or a shared context value. Then the compiler caches a result whose real input changed behind its back and the UI goes stale. That is why the compiler's ESLint rule is a precondition rather than a nicety: `eslint-plugin-react-compiler` surfaces the same diagnostics in the editor so you fix them before the build starts trusting them.
go deeper
Know the one-line rule: components must be pure during render — no mutating props or state, no side effects — and that the compiler depends on it. Being able to say why memoization needs purity is enough at this level.
Explain the mechanism both ways: a detected violation makes the compiler skip that function, while an unseen mutation lets it reuse a cached result whose real input changed. Name mutation-during-render and conditional hook calls as the concrete violations.
Show a debugging path — bisect with the opt-out directive, confirm which components actually compiled, then fix the impurity rather than shipping the escape hatch. Point out that the impurity was already a latent bug before the compiler stopped hiding it.
Own the organisational move: turning purity from a review convention into a build-enforced invariant means the lint rule blocks merges and the violation backlog is scheduled work. Decide whether the team can hold that bar before committing the codebase to auto-memoization.
## Why purity is the precondition Memoization is a bet: if the inputs did not change, the output cannot have changed, so it is safe to reuse the previous result. That bet is only valid when the function is *pure* — its result depends solely on its inputs, and running it has no effect anyone else can observe. Hand-written `useMemo` makes the same bet, but a human makes it one call site at a time and can eyeball the code. The React Compiler makes it everywhere, automatically. So the properties that were previously "good practice" become the compiler's correctness contract. ## The properties it relies on **Render is pure.** Given the same props, state and context, a component returns the same JSX. No random values, no reading a clock or a global mutable counter to decide what to show. **No mutation during render.** A component must not mutate its props, its state, values from context, or any object it already returned in JSX. Creating and mutating a *local* object before returning it is fine — the object is not observable elsewhere yet. The rule is about mutating things that already have another owner. **No side effects during render.** No writing to refs, no subscribing, no calling a setter of another component, no touching the DOM. Those belong in event handlers or effects. **Hooks are called unconditionally, at the top level of a component or another hook.** The compiler needs a fixed, statically known shape for the function to place its cache slots. This is the Rules of Hooks constraint that was always there; the compiler simply cannot work around a violation. ## Two very different failure modes **Detected violation: the compiler bails out.** The compiler is conservative. When its analysis cannot prove a function is safe to memoize — a conditional hook call, a mutation pattern it recognises, a construct it does not model — it skips that function and emits it unchanged. Your app still works; that component is simply not optimised. This is the common case, and it is why enabling the compiler on a messy codebase is usually uneventful rather than catastrophic: you get less benefit, not broken behaviour. **Undetected violation: stale UI.** The genuinely bad case is a violation static analysis cannot see. The classic shape: ```jsx function Chart({ config }) { config.points = compute(config.raw); // mutating a prop during render return <Canvas points={config.points} />; } ``` The compiler sees `config` unchanged by reference on the next render and reuses the cached `<Canvas />`, but the real content depends on a mutation it did not model. The screen shows last render's data. The same shape appears with a module-level object mutated during render, or an array from context sorted in place. These bugs are hard to attribute because the *compiler* looks like the culprit while the mutation is the actual defect — the uncompiled build merely hid it by re-running everything every time. ## Finding violations before the build depends on them Run the compiler's lint rule and treat its findings as blocking. `eslint-plugin-react-compiler` runs the same analysis the compiler runs and reports it in the editor, so a mutation-during-render or a conditional hook call shows up while you are writing it rather than as a stale pixel two sprints later. Fixing what it reports has a second payoff: every violation you clear is a component that moves from "skipped" to "compiled", so the lint burn-down is also the optimisation burn-down. When you are diagnosing a suspected miscompilation, the escape hatch is the `"use no memo"` directive on the one function, used to bisect. If opting a component out fixes the bug, you have localised it — and the next step is finding the impurity in that component, not leaving the directive there forever. ## What an interviewer is listening for The weak answer is "the compiler needs clean code". The strong answer names the specific property (purity, no mutation of things you do not own during render), distinguishes the conservative bail-out from the silent staleness, and says that the lint rule is the mechanism that turns a convention into an enforced precondition. Bonus points for the observation that these bugs pre-existed the compiler — an impure render was always a latent bug; auto-memoization only stops paying to hide it.
- Is creating and mutating an object inside a component's render body always a violation?No. Mutating a *local* object you just created and have not yet handed to anyone — building an array before returning it in JSX, for example — is fine, because nothing else can observe the intermediate states. The rule bites when you mutate something you did not create in this render: a prop, a context value, a module-level object, or a value you already returned.
- If a component is skipped by the compiler, how would you find that out?React DevTools badges components the compiler optimised, so a component missing that marker in an otherwise compiled build is a skipped one. The build-time logging the plugin exposes reports the functions it bailed out on and why, and the compiler's lint rule usually flags the underlying reason in the editor first.
- A team reports a bug that disappears when they disable the compiler. What is your first hypothesis?That the component depends on being re-run every render — an impure render, most often a mutation of a prop, a context value or a module-level object, or a value read from a ref during render. Bisect with the "use no memo" directive to localise the function, then fix the impurity rather than shipping the opt-out.
- Why does the compiler skip a component instead of failing the build?Because bailing out is always safe: uncompiled output is exactly the code you wrote, so the app behaves as it did before. Hard-failing on every unmodelled construct would make incremental adoption impossible in a large codebase. Its error-strictness is configurable, but the conservative default is what lets you turn it on gradually.
saying these in an interview costs you the question
- Says the compiler fixes impure components for you
- Thinks a rule violation always fails the build loudly
- Claims mutating props is fine because React copies them
- Treats the compiler's lint rule as optional style advice
- Blames the compiler for a bug caused by mutation during render