Skip to content

Commit b3dc63b

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 990da53 commit b3dc63b

File tree

4 files changed

+205
-6
lines changed

4 files changed

+205
-6
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.new.js

+31-3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {NoWork, Sync} from './ReactFiberExpirationTime';
3737
import {NoMode, BlockingMode} from './ReactTypeOfMode';
3838
import {readContext} from './ReactFiberNewContext.new';
3939
import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.new';
40+
import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack';
4041
import {
4142
Update as UpdateEffect,
4243
Passive as PassiveEffect,
@@ -1184,12 +1185,39 @@ function pushEffect(tag, create, destroy, deps) {
11841185

11851186
function mountRef<T>(initialValue: T): {|current: T|} {
11861187
const hook = mountWorkInProgressHook();
1187-
const ref = {current: initialValue};
11881188
if (__DEV__) {
1189+
const fiber = currentlyRenderingFiber;
1190+
let current = initialValue;
1191+
let shouldWarnAboutRead = false;
1192+
const ref = {
1193+
get current() {
1194+
if (shouldWarnAboutRead && currentlyRenderingFiber !== null) {
1195+
console.warn(
1196+
'%s: Unsafe read of a mutable value during render.\n\n' +
1197+
'Reading from a ref during render is only safe if:\n' +
1198+
'1. The ref value has not been updated, or\n' +
1199+
'2. The ref holds a lazily-initialized value that is only set once.\n\n%s',
1200+
getComponentName(fiber.type) || 'Unknown',
1201+
getStackByFiberInDevAndProd(fiber),
1202+
);
1203+
}
1204+
return current;
1205+
},
1206+
set current(value) {
1207+
if (!shouldWarnAboutRead && current != null) {
1208+
shouldWarnAboutRead = true;
1209+
}
1210+
current = value;
1211+
},
1212+
};
11891213
Object.seal(ref);
1214+
hook.memoizedState = ref;
1215+
return ref;
1216+
} else {
1217+
const ref = {current: initialValue};
1218+
hook.memoizedState = ref;
1219+
return ref;
11901220
}
1191-
hook.memoizedState = ref;
1192-
return ref;
11931221
}
11941222

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

packages/react-reconciler/src/ReactFiberHooks.old.js

+31-3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {NoWork, Sync} from './ReactFiberExpirationTime';
3737
import {NoMode, BlockingMode} from './ReactTypeOfMode';
3838
import {readContext} from './ReactFiberNewContext.old';
3939
import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.old';
40+
import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack';
4041
import {
4142
Update as UpdateEffect,
4243
Passive as PassiveEffect,
@@ -1184,12 +1185,39 @@ function pushEffect(tag, create, destroy, deps) {
11841185

11851186
function mountRef<T>(initialValue: T): {|current: T|} {
11861187
const hook = mountWorkInProgressHook();
1187-
const ref = {current: initialValue};
11881188
if (__DEV__) {
1189+
const fiber = currentlyRenderingFiber;
1190+
let current = initialValue;
1191+
let shouldWarnAboutRead = false;
1192+
const ref = {
1193+
get current() {
1194+
if (shouldWarnAboutRead && currentlyRenderingFiber !== null) {
1195+
console.warn(
1196+
'%s: Unsafe read of a mutable value during render.\n\n' +
1197+
'Reading from a ref during render is only safe if:\n' +
1198+
'1. The ref value has not been updated, or\n' +
1199+
'2. The ref holds a lazily-initialized value that is only set once.\n\n%s',
1200+
getComponentName(fiber.type) || 'Unknown',
1201+
getStackByFiberInDevAndProd(fiber),
1202+
);
1203+
}
1204+
return current;
1205+
},
1206+
set current(value) {
1207+
if (!shouldWarnAboutRead && current != null) {
1208+
shouldWarnAboutRead = true;
1209+
}
1210+
current = value;
1211+
},
1212+
};
11891213
Object.seal(ref);
1214+
hook.memoizedState = ref;
1215+
return ref;
1216+
} else {
1217+
const ref = {current: initialValue};
1218+
hook.memoizedState = ref;
1219+
return ref;
11901220
}
1191-
hook.memoizedState = ref;
1192-
return ref;
11931221
}
11941222

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

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

+140
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,142 @@ 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([
151+
'Example: Unsafe read of a mutable value during render.',
152+
]);
153+
154+
return value;
155+
}
156+
157+
act(() => {
158+
ReactNoop.render(<Example />);
159+
});
160+
});
161+
162+
it('should not warn about lazy init during render', () => {
163+
function Example() {
164+
const ref1 = useRef(null);
165+
const ref2 = useRef();
166+
if (ref1.current === null) {
167+
// Read 1: safe because null
168+
ref1.current = 123;
169+
ref2.current = 123;
170+
}
171+
return ref1.current + ref2.current; // Read 2: safe because lazy init
172+
}
173+
174+
act(() => {
175+
ReactNoop.render(<Example />);
176+
});
177+
});
178+
179+
it('should not warn about lazy init outside of render', () => {
180+
function Example() {
181+
// eslint-disable-next-line no-unused-vars
182+
const [didMount, setDidMount] = useState(false);
183+
const ref1 = useRef(null);
184+
const ref2 = useRef();
185+
useLayoutEffect(() => {
186+
ref1.current = 123;
187+
ref2.current = 123;
188+
setDidMount(true);
189+
}, []);
190+
return ref1.current + ref2.current; // Read 2: safe because lazy init
191+
}
192+
193+
act(() => {
194+
ReactNoop.render(<Example />);
195+
});
196+
});
197+
198+
it('should warn about updates to ref after lazy init pattern', () => {
199+
function Example() {
200+
const ref1 = useRef(null);
201+
const ref2 = useRef();
202+
if (ref1.current === null) {
203+
// Read 1: safe because null
204+
ref1.current = 123;
205+
ref2.current = 123;
206+
}
207+
expect(ref1.current).toBe(123); // Read 2: safe because lazy init
208+
expect(ref2.current).toBe(123); // Read 2: safe because lazy init
209+
210+
ref1.current = 456; // Second mutation, now reads will be unsafe
211+
ref2.current = 456; // Second mutation, now reads will be unsafe
212+
213+
expect(() => {
214+
expect(ref1.current).toBe(456); // Read 3: unsafe because mutation
215+
}).toWarnDev([
216+
'Example: Unsafe read of a mutable value during render.',
217+
]);
218+
expect(() => {
219+
expect(ref2.current).toBe(456); // Read 3: unsafe because mutation
220+
}).toWarnDev([
221+
'Example: Unsafe read of a mutable value during render.',
222+
]);
223+
224+
return null;
225+
}
226+
227+
act(() => {
228+
ReactNoop.render(<Example />);
229+
});
230+
});
231+
232+
it('should not warn about reads within effect', () => {
233+
function Example() {
234+
const ref = useRef(123);
235+
ref.current = 456;
236+
useLayoutEffect(() => {
237+
expect(ref.current).toBe(456);
238+
}, []);
239+
useEffect(() => {
240+
expect(ref.current).toBe(456);
241+
}, []);
242+
return null;
243+
}
244+
245+
act(() => {
246+
ReactNoop.render(<Example />);
247+
});
248+
249+
ReactNoop.flushPassiveEffects();
250+
});
251+
252+
it('should not warn about reads outside of render phase (e.g. event handler)', () => {
253+
let ref;
254+
function Example() {
255+
ref = useRef(123);
256+
ref.current = 456;
257+
return null;
258+
}
259+
260+
act(() => {
261+
ReactNoop.render(<Example />);
262+
});
263+
264+
expect(ref.current).toBe(456);
265+
});
266+
}
127267
});

0 commit comments

Comments
 (0)