useRef holds values not needed for rendering, so why should you not read it during render?
Two sentences in the React docs looked contradictory at first. useRef stores values not needed for rendering, yet React says not to read ref.current during render. Tracing the source code shows these are about two different dimensions of rendering: trigger vs consistency, all the way to tearing.
6 min readreactuseRefrendering
Published
Mar 01, 2026
Reading time
6 min
Sections
11
On this page11+-
useRef holds values not needed for rendering, so why shouldn't you read it during render?
While reading the React docs for useRef, I got stuck between two sentences.
useRef is a React Hook that lets you reference a value that's not needed for rendering.
Do not write or read ref.current during rendering, except for initialization. This makes your component's behavior unpredictable.
If a value is not needed for rendering, why should reading it during render be forbidden? It sounded like “a value that does not affect rendering can still affect rendering,” which felt contradictory.
After tracing the source code, it turned out not to be a contradiction. The word “rendering” points to different things in those two sentences.
Two dimensions of “rendering”
The two sentences are talking about different dimensions.
Sentence
Dimension
Meaning
"Not needed for rendering"
Trigger
Changing ref.current does not trigger a re-render
"Do not read during rendering"
Consistency
In concurrent rendering, ref values are not guaranteed to stay stable
during a render
The first sentence is about trigger behavior: changing ref.current does not cause a re-render. useState has a path that tells React “render again” through setState, but useRef has no such path.
The second sentence is about consistency in concurrent rendering: when rendering is interrupted and resumed, ref.current can change in the middle. Because it is mutable data outside React’s control, each render attempt can read a different value.
So the same word “rendering” points to different mechanisms. “Does not trigger rendering” and “safe to read during rendering” are separate properties. We can verify both in source.
useState notifies React through dispatch -> dispatchSetState -> scheduleUpdateOnFiber. useRef has no such path at all. That is the real meaning of “not needed for rendering” in the docs: even if you mutate ref.current, there is no mechanism to schedule a re-render.
That covers the trigger dimension. Now to consistency.
What goes wrong if you read during render
Reading values not integrated into React’s render pipeline creates problems at three levels.
cachedDataRef.current = null -> <Loading /> is shown.
Fetch completes -> cachedDataRef.current = data in an event handler (no re-render).
setUserName("Park") -> re-render happens.
Only now does <DataView> appear.
The timing of UI updates depends on unrelated state changes. If userName never changes, data may never appear. This creates bugs that are very hard to debug because display timing is outside explicit control.
Problem 3: Tearing
The most serious problem appears in concurrent rendering. React can split rendering into time slices. It can render the upper part of the tree, yield, and later continue with the lower part.
Child1 renders in time slice 1 and reads ref.current = "A". Then an event updates it to "B". Child2 renders in time slice 2 and reads ref.current = "B".
Same render pass, different values. One part of the UI sees “A,” another sees “B.” That is tearing.
useState does not have this issue because React fixes a state snapshot at the start of render. This is exactly why useSyncExternalStore exists for safely reading external mutable values in render. It checks whether the snapshot changed during render via checkIfSnapshotChanged (ReactFiberHooks.js:1876), and forces a synchronous re-render if needed.
Level
Symptom
Severity
Problem 1
Ref changes are not reflected in UI
Low (intended behavior)
Problem 2
Values appear only when unrelated state changes happen
Medium (hard to debug)
Problem 3
Tearing: different values in the same render
High (breaks UI consistency)
Why is initialization an exception?
The docs explicitly allow an exception: “except for initialization.” This does not mean concurrent rendering is turned off during initialization. It still works the same way. The exception is allowed because of idempotency.
function Example() {
const ref = useRef(null);
if (ref.current === null) {
ref.current = new ExpensiveObject();
}
// ...
}
In this pattern, ref.current only moves one way: null -> value. Even if React discards and retries render, once ref.current has a value the condition is skipped. No matter how many retries happen, the result stays the same.
Compared with regular “read ref during render,” the difference is clear.
[Normal read during render]
render#1: ref.current = "A" -> JSX("A")
-- React discards render#1, event handler sets ref.current = "B" --
render#2: ref.current = "B" -> JSX("B") <- different result!
[Initialization pattern]
render#1: ref.current === null -> ref.current = new X() -> JSX(...)
-- React discards render#1 --
render#2: ref.current !== null -> keep existing value -> JSX(...) <- same result
React test code explicitly allows this pattern.
// packages/react-reconciler/src/__tests__/useRef-test.internal.js:158-180
it('should not warn about lazy init during render', async () => {
function Example() {
const ref1 = useRef(null);
const ref2 = useRef(undefined);
// Read: safe because lazy init:
if (ref1.current === null) {
ref1.current = 123;
}
if (ref2.current === undefined) {
ref2.current = 123;
}
return null;
}
await act(() => {
ReactNoop.render(<Example />);
});
// Should not warn after an update either.
await act(() => {
ReactNoop.render(<Example />);
});
});
The React team even left the comment // Read: safe because lazy init:. It is not “safe because concurrent rendering is off,” but “safe because retries still produce the same result in concurrent rendering.”
Why isn't this rule enforced by code?
It is also worth asking why such a risky rule is not enforced by runtime errors.
A similar rule, “state must be immutable,” is also not strictly enforced by code.
const [user, setUser] = useState({ name: 'Kim' });
user.name = 'Park'; // no JS error, no React error
Even with direct mutation, JavaScript does not block it and React runtime does not throw. The mutation itself is not detected by React, so the UI does not update immediately. (If another re-render happens for a different reason, it can show up later.)
State immutability rule
Do not read refs for render output
Enforced by JS
X (objects are freely mutable)
X (ref.current can be freely read)
Enforced by React runtime
X (no error on mutation)
X (no error on reads)
DEV warning
StrictMode double render can make symptoms easier to notice
Pre-detection via ESLint rule (react-hooks/refs)
If you violate it
React can miss the change -> no immediate UI update
Accidental reflection, tearing
Both break React’s core assumption: “render is a pure function.” Since JavaScript cannot block these patterns at language level, React relies on documentation and lint rules. Keeping that assumption is the developer’s responsibility.
Wrap-up
Back to the original question:
“Not needed for rendering,” yet “do not read during rendering” -- is this contradictory?
It is not. The two sentences are the two sides of one design intent. “Made for use outside rendering” (feature definition) naturally implies “do not use it to decide render output” (usage rule).
useState
useRef (read during render)
Value change -> re-render
Guaranteed (scheduleUpdateOnFiber)
No trigger path
Consistency in same render
Guaranteed (fixed snapshot)
Not guaranteed (mutable)
Concurrent tearing
Prevented
Exposed
The structure of mountRef (ReactFiberHooks.js:2602) shows this clearly: no update queue, no dispatch, and intentionally no scheduleUpdateOnFiber path. The code itself reflects the design decision not to connect refs to the render scheduling pipeline.
The moment this value starts affecting render output, it leaves React’s safety boundaries: fixed snapshots, bailout optimizations, tearing prevention, and more. So “do not read it” more precisely means: do not use it to decide render output.