Skip to content

Commit b1d648c

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 b1d648c

File tree

3 files changed

+161
-3
lines changed

3 files changed

+161
-3
lines changed

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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 31 additions & 3 deletions
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';
3939
import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents';
40+
import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack';
4041
import {
4142
Update as UpdateEffect,
4243
Passive as PassiveEffect,
@@ -1233,12 +1234,39 @@ function pushEffect(tag, create, destroy, deps) {
12331234
12341235
function mountRef<T>(initialValue: T): {|current: T|} {
12351236
const hook = mountWorkInProgressHook();
1236-
const ref = {current: initialValue};
12371237
if (__DEV__) {
1238+
const fiber = currentlyRenderingFiber;
1239+
let current = initialValue;
1240+
let shouldWarnAboutRead = false;
1241+
const ref = {
1242+
get current() {
1243+
if (shouldWarnAboutRead && currentlyRenderingFiber !== null) {
1244+
console.warn(
1245+
'%s: Unsafe read of a mutable value during render.\n\n' +
1246+
'Reading from a ref during render is only safe if:\n' +
1247+
'1. The ref value has not been updated, or\n' +
1248+
'2. The ref holds a lazily-initialized value that is only set once.\n\n%s',
1249+
getComponentName(fiber.type) || 'Unknown',
1250+
getStackByFiberInDevAndProd(fiber),
1251+
);
1252+
}
1253+
return current;
1254+
},
1255+
set current(value) {
1256+
if (!shouldWarnAboutRead && current !== null) {
1257+
shouldWarnAboutRead = true;
1258+
}
1259+
current = value;
1260+
},
1261+
};
12381262
Object.seal(ref);
1263+
hook.memoizedState = ref;
1264+
return ref;
1265+
} else {
1266+
const ref = {current: initialValue};
1267+
hook.memoizedState = ref;
1268+
return ref;
12391269
}
1240-
hook.memoizedState = ref;
1241-
return ref;
12421270
}
12431271

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

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

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

0 commit comments

Comments
 (0)