In React, why is reading or writing ref.current during rendering unsafe, and what is the accepted exception for lazily creating an expensive ref value?
answer
- render may be run twice or thrown away
- refs are untracked and never reverted
- a discarded render still left the write
- idempotent first-time fill is invisible
- null-guard, not a fresh value each render
basics
~20 sRendering must be pure and repeatable: React may run a component twice, discard the result, or restart it. A ref is untracked mutable state, so render-phase reads can observe a value from a render nobody saw, and writes make the render impure. The one sanctioned exception is initialising a ref that is still null.
solid answer
~50 sReact treats a component's body as a pure function of props, state and context — it may call it more than once for the same update, throw the result away, or restart it, and it makes no promise about how many times or in what order. A ref is deliberately outside that model: React neither tracks reads nor reverts writes. So writing during render makes the render a side effect, and its result depends on how many times React happened to call you; reading during render can hand you a value written by a render that never committed. Both are fine in event handlers and effects, where the work is ordered and committed. The exception the React docs sanction is lazy first-time initialisation — `if (ref.current === null) ref.current = createSomething()` — because it is idempotent, invisible to the output, and only fills a box that was empty.
code
javascript · 12 linesimport { useRef, useEffect } from 'react';
export function usePrevious(value) {
const prevRef = useRef(null);
// committed renders only: safe place to advance the marker
useEffect(() => {
prevRef.current = value;
}, [value]);
return prevRef.current;
}go deeper
Take away the practical rule: put ref reads and writes in event handlers or effects, not in the component body. Recognise that a DOM ref is empty during render anyway.
Explain the mechanism behind the rule — render must be a pure, repeatable function, and a ref is untracked mutable state React neither observes nor reverts — and know that StrictMode double-invocation exposes violations.
Diagnose the real failure: a discarded or repeated render leaves the mutation behind, so downstream comparisons act on renders nobody saw. Justify the null-guard exception on idempotence grounds rather than reciting it.
Connect purity to capability: interruptible rendering, priority scheduling and speculative work all require the framework to discard renders freely. Be ready to say how you keep an escape hatch like refs from eroding that guarantee across a large codebase.
## The rule React is protecting React's rendering model rests on a promise you make: given the same props, state and context, your component body returns the same output and does nothing else observable. React leans on that promise hard. It may render a component and discard the result because a higher-priority update arrived. It may start rendering, pause, and restart from the top. In development it deliberately double-invokes component bodies under StrictMode to surface impurity. None of that is safe if your component's body mutates something the rest of the app can see. A ref is precisely such a thing. It is an untracked mutable box, by design: React never observes writes to it and never rolls them back. So a component that mutates `ref.current` during render mutates shared program state a variable number of times per update, with no way to undo it. ## What goes wrong when you write during render ```jsx function Chart({ data }) { const renderCount = useRef(0); renderCount.current++; // impure: runs a variable number of times return <svg>{/* ... */}</svg>; } ``` The count is not "how many times the user saw this component" — it is "how many times React chose to call the function", which doubles in StrictMode development and changes with concurrent scheduling. Any logic that branches on it is now scheduling-dependent, which is the worst class of bug to reproduce. The subtler version writes a *meaningful* value: ```jsx prevValue.current = value; // during render ``` If this render is discarded, `prevValue.current` has still advanced. The next render that does commit now compares against a value from a render that never reached the screen, and the "previous value" logic silently reports no change. The correct home for that assignment is an effect, which runs only after a render commits. ## What goes wrong when you read during render Reading is less obviously harmful but breaks the same contract from the other side. `ref.current` is not part of the render's inputs, so React has no reason to re-render when it changes — output derived from it can be arbitrarily stale, and there is no mechanism that will ever correct it. Under concurrent rendering, a read can also observe a write made by a render that was thrown away. In practice you see it as UI that is correct on some paths and stale on others, with no state change to blame. DOM refs add a timing dimension: during render there is no committed node yet, so a fresh `ref.current` for a DOM element is `null` anyway. ## The sanctioned exception: lazy initialisation Some ref contents are expensive to build — an `IntersectionObserver`, a parser, a large map — and `useRef(new Expensive())` constructs a throwaway on every render because JavaScript evaluates the argument even though React ignores it after the first render. The documented workaround writes to the ref during render, under one guard: ```jsx function Watcher() { const observerRef = useRef(null); if (observerRef.current === null) { observerRef.current = new IntersectionObserver(handleEntries); } // ... } ``` This is acceptable because the write is **idempotent and invisible**. It happens at most once per ref object no matter how many times the body runs, it produces the same box contents on every path, and nothing in the rendered output depends on whether this particular call performed it. Repeat the render twice under StrictMode and the second pass is a no-op. That is the whole test — not "writes during render are sometimes fine", but "a write that cannot be observed to have happened more or fewer times is fine". Note what it does *not* license: `if (ref.current === null)` around an expression that varies with props, or a guard that resets the ref, or `??=` used to "refresh" a value each render. ## Where each operation actually belongs - **Writing a value derived from this render** — an effect, which runs after commit, so it observes only renders the user actually got. - **Reading a DOM node** — an effect or a handler, once the node has been attached. - **Reading a mutable value in response to a user action** — the event handler, where nothing is speculative. - **Creating an expensive ref value once** — the null-guard above, or `useState` with a lazy initialiser when the value never changes and you would rather not think about it. ## Why interviewers ask The question separates people who learned "don't mutate refs while rendering" as a rule from people who can say *what breaks*: a render that is not a pure function of its inputs cannot be discarded, repeated, or reordered, and every one of React's concurrent capabilities depends on being able to do exactly that.
- Where should a 'previous value of this prop' assignment go instead of the render body?In an effect. Effects run only after a render commits, so `prevRef.current = value` there records values the user actually saw. Assigning during render advances the marker even for renders React discarded, which makes the next comparison report no change when there really was one.
- Why is `if (ref.current === null) ref.current = new Thing()` acceptable when other render-phase writes are not?Because it is idempotent and unobservable. It fires at most once per ref object however many times React calls the body, the rendered output never depends on which call did it, and repeats are no-ops. A write whose effect varies with call count fails all three tests.
- How does StrictMode help you find these bugs?In development it deliberately invokes component bodies twice, so anything that counts, accumulates or advances during render produces visibly doubled results. It cannot catch every impurity, but a render-phase ref write is exactly the shape it is designed to expose.
saying these in an interview costs you the question
- Uses a render-phase ref increment to count renders
- Says refs are safe anywhere because React ignores them
- Assigns the previous prop value during render instead of in an effect
- Thinks render runs exactly once per update
- Treats any null-guarded write as licence to mutate during render