Skip to content

Commit ee03c74

Browse files
author
Brian Vaughn
committed
useRef: Warn if reading mutable value during render
Reading from a ref during render is only safe if: 1. The ref value has not been updated, or 2. The ref holds a lazily-initialized value that is only set once. This PR adds a new DEV warning to detect unsaf reads.
1 parent 5b1d0f0 commit ee03c74

File tree

3 files changed

+136
-3
lines changed

3 files changed

+136
-3
lines changed

packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js

+3
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,9 @@ describe('ReactDOMServerHooks', () => {
503503
return <span>Count: {ref.current}</span>;
504504
}
505505

506+
// Reading from ref during render (after a mutation) triggers a warning.
507+
spyOnDev(console, 'warn');
508+
506509
const domNode = await render(<Counter />);
507510
expect(clearYields()).toEqual([0, 1, 2, 3]);
508511
expect(domNode.textContent).toEqual('Count: 3');

packages/react-reconciler/src/ReactFiberHooks.js

+27-3
Original file line numberDiff line numberDiff line change
@@ -1233,12 +1233,36 @@ function pushEffect(tag, create, destroy, deps) {
12331233

12341234
function mountRef<T>(initialValue: T): {|current: T|} {
12351235
const hook = mountWorkInProgressHook();
1236-
const ref = {current: initialValue};
12371236
if (__DEV__) {
1237+
let current = initialValue;
1238+
let shouldWarnAboutRead = false;
1239+
const ref = {
1240+
get current() {
1241+
if (shouldWarnAboutRead && currentlyRenderingFiber !== null) {
1242+
console.warn(
1243+
'useRef: Unsafe read of a mutable value during render.\n\n' +
1244+
'Reading from a ref during render is only safe if:\n' +
1245+
'1. The ref value has not been updated, or\n' +
1246+
'2. The ref holds a lazily-initialized value that is only set once.',
1247+
);
1248+
}
1249+
return current;
1250+
},
1251+
set current(value) {
1252+
if (!shouldWarnAboutRead && current !== null) {
1253+
shouldWarnAboutRead = true;
1254+
}
1255+
current = value;
1256+
},
1257+
};
12381258
Object.seal(ref);
1259+
hook.memoizedState = ref;
1260+
return ref;
1261+
} else {
1262+
const ref = {current: initialValue};
1263+
hook.memoizedState = ref;
1264+
return ref;
12391265
}
1240-
hook.memoizedState = ref;
1241-
return ref;
12421266
}
12431267

12441268
function updateRef<T>(initialValue: T): {|current: T|} {

packages/react-reconciler/src/__tests__/useRef-test.internal.js

+106
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('useRef', () => {
1919
let act;
2020
let useCallback;
2121
let useEffect;
22+
let useLayoutEffect;
2223
let useRef;
2324
let useState;
2425

@@ -33,6 +34,7 @@ describe('useRef', () => {
3334
act = ReactNoop.act;
3435
useCallback = React.useCallback;
3536
useEffect = React.useEffect;
37+
useLayoutEffect = React.useLayoutEffect;
3638
useRef = React.useRef;
3739
useState = React.useState;
3840
});
@@ -124,4 +126,108 @@ describe('useRef', () => {
124126
ReactNoop.render(<Counter />);
125127
expect(Scheduler).toFlushAndYield(['val']);
126128
});
129+
130+
if (__DEV__) {
131+
it('should not warn about reads if value is not mutated', () => {
132+
function Example() {
133+
const ref = useRef(123);
134+
return ref.current;
135+
}
136+
137+
act(() => {
138+
ReactNoop.render(<Example />);
139+
});
140+
});
141+
142+
it('should warn about reads during render phase if value has been mutated', () => {
143+
function Example() {
144+
const ref = useRef(123);
145+
ref.current = 456;
146+
147+
let value;
148+
expect(() => {
149+
value = ref.current;
150+
}).toWarnDev(['useRef: Unsafe read of a mutable ref during render.']);
151+
152+
return value;
153+
}
154+
155+
act(() => {
156+
ReactNoop.render(<Example />);
157+
});
158+
});
159+
160+
it('should not warn about lazy init pattern', () => {
161+
function Example() {
162+
const ref = useRef(null);
163+
if (ref.current === null) {
164+
// Read 1: safe because null
165+
ref.current = 123;
166+
}
167+
return ref.current; // Read 2: safe because lazy init
168+
}
169+
170+
act(() => {
171+
ReactNoop.render(<Example />);
172+
});
173+
});
174+
175+
it('should warn about updates to ref after lazy init pattern', () => {
176+
function Example() {
177+
const ref = useRef(null);
178+
if (ref.current === null) {
179+
// Read 1: safe because null
180+
ref.current = 123;
181+
}
182+
expect(ref.current).toBe(123); // Read 2: safe because lazy init
183+
184+
ref.current = 456; // Second mutation, now reads will be unsafe
185+
186+
expect(() => {
187+
expect(ref.current).toBe(456); // Read 3: unsafe because mutation
188+
}).toWarnDev(['useRef: Unsafe read of a mutable ref during render.']);
189+
190+
return null;
191+
}
192+
193+
act(() => {
194+
ReactNoop.render(<Example />);
195+
});
196+
});
197+
198+
it('should not warn about reads witin effect', () => {
199+
function Example() {
200+
const ref = useRef(123);
201+
ref.current = 456;
202+
useLayoutEffect(() => {
203+
expect(ref.current).toBe(456);
204+
}, []);
205+
useEffect(() => {
206+
expect(ref.current).toBe(456);
207+
}, []);
208+
return null;
209+
}
210+
211+
act(() => {
212+
ReactNoop.render(<Example />);
213+
});
214+
215+
ReactNoop.flushPassiveEffects();
216+
});
217+
218+
it('should not warn about reads outside of render phase (e.g. event handler)', () => {
219+
let ref;
220+
function Example() {
221+
ref = useRef(123);
222+
ref.current = 456;
223+
return null;
224+
}
225+
226+
act(() => {
227+
ReactNoop.render(<Example />);
228+
});
229+
230+
expect(ref.current).toBe(456);
231+
});
232+
}
127233
});

0 commit comments

Comments
 (0)