useRef: Warn about reading or writing mutable values during render (#18545)
Reading or writing a ref value during render is only safe if you are implementing the lazy initialization pattern. Other types of reading are unsafe as the ref is a mutable source. Other types of writing are unsafe as they are effectively side effects. This change also refactors useTransition to no longer use a ref hook, but instead manage its own (stable) hook state.
Brian Vaughn committed
Oct 19, 2020 at 16:05 UTC
c59c3dfe554dafb64864f3bbcfff6ffe51f32275
15 files changed
+567
-121
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
+8
-10
@@ -474,12 +474,12 @@ describe('ReactDOMServerHooks', () => {
474
describe('useRef', () => {
475
itRenders('basic render', async render => {
476
function Counter(props) {
477
- const count = useRef(0);
478
- return <span>Count: {count.current}</span>;
477
+ const ref = useRef();
478
+ return <span ref={ref}>Hi</span>;
479
}
480
481
const domNode = await render(<Counter />);
482
- expect(domNode.textContent).toEqual('Count: 0');
482
+ expect(domNode.textContent).toEqual('Hi');
483
});
484
485
itRenders(
@@ -487,18 +487,16 @@ describe('ReactDOMServerHooks', () => {
487
async render => {
488
function Counter(props) {
489
const [count, setCount] = useState(0);
490
- const ref = useRef(count);
490
+ const ref = useRef();
491
492
if (count < 3) {
493
const newCount = count + 1;
494
-
495
- ref.current = newCount;
494
setCount(newCount);
495
}
496
497
yieldValue(count);
498
501
- return <span>Count: {ref.current}</span>;
499
+ return <span ref={ref}>Count: {count}</span>;
500
}
501
502
const domNode = await render(<Counter />);
@@ -513,7 +511,7 @@ describe('ReactDOMServerHooks', () => {
511
let firstRef = null;
512
function Counter(props) {
513
const [count, setCount] = useState(0);
516
- const ref = useRef(count);
514
+ const ref = useRef();
515
if (firstRef === null) {
516
firstRef = ref;
517
} else if (firstRef !== ref) {
@@ -528,12 +526,12 @@ describe('ReactDOMServerHooks', () => {
526
527
yieldValue(count);
528
531
- return <span>Count: {ref.current}</span>;
529
+ return <span ref={ref}>Count: {count}</span>;
530
}
531
532
const domNode = await render(<Counter />);
533
expect(clearYields()).toEqual([0, 1, 2, 3]);
536
- expect(domNode.textContent).toEqual('Count: 0');
534
+ expect(domNode.textContent).toEqual('Count: 3');
535
},
536
);
537
});
packages/react-reconciler/src/ReactFiberHooks.new.js
+91
-12
@@ -27,6 +27,7 @@ import {
27
enableNewReconciler,
28
decoupleUpdatePriorityFromScheduler,
29
enableDoubleInvokingEffects,
30
+ enableUseRefAccessWarning,
31
} from 'shared/ReactFeatureFlags';
32
33
import {
@@ -1197,14 +1198,92 @@ function pushEffect(tag, create, destroy, deps) {
1198
return effect;
1199
}
1200
1201
+let stackContainsErrorMessage: boolean | null = null;
1202
+
1203
+function getCallerStackFrame(): string {
1204
+ const stackFrames = new Error('Error message').stack.split('\n');
1205
+
1206
+ // Some browsers (e.g. Chrome) include the error message in the stack
1207
+ // but others (e.g. Firefox) do not.
1208
+ if (stackContainsErrorMessage === null) {
1209
+ stackContainsErrorMessage = stackFrames[0].includes('Error message');
1210
+ }
1211
+
1212
+ return stackContainsErrorMessage
1213
+ ? stackFrames.slice(3, 4).join('\n')
1214
+ : stackFrames.slice(2, 3).join('\n');
1215
+}
1216
+
1217
function mountRef<T>(initialValue: T): {|current: T|} {
1218
const hook = mountWorkInProgressHook();
1202
- const ref = {current: initialValue};
1203
- if (__DEV__) {
1204
- Object.seal(ref);
1219
+ if (enableUseRefAccessWarning) {
1220
+ if (__DEV__) {
1221
+ // Support lazy initialization pattern shown in docs.
1222
+ // We need to store the caller stack frame so that we don't warn on subsequent renders.
1223
+ let hasBeenInitialized = initialValue != null;
1224
+ let lazyInitGetterStack = null;
1225
+ let didCheckForLazyInit = false;
1226
+
1227
+ // Only warn once per component+hook.
1228
+ let didWarnAboutRead = false;
1229
+ let didWarnAboutWrite = false;
1230
+
1231
+ let current = initialValue;
1232
+ const ref = {
1233
+ get current() {
1234
+ if (!hasBeenInitialized) {
1235
+ didCheckForLazyInit = true;
1236
+ lazyInitGetterStack = getCallerStackFrame();
1237
+ } else if (currentlyRenderingFiber !== null && !didWarnAboutRead) {
1238
+ if (
1239
+ lazyInitGetterStack === null ||
1240
+ lazyInitGetterStack !== getCallerStackFrame()
1241
+ ) {
1242
+ didWarnAboutRead = true;
1243
+ console.warn(
1244
+ '%s: Unsafe read of a mutable value during render.\n\n' +
1245
+ 'Reading from a ref during render is only safe if:\n' +
1246
+ '1. The ref value has not been updated, or\n' +
1247
+ '2. The ref holds a lazily-initialized value that is only set once.\n',
1248
+ getComponentName(currentlyRenderingFiber.type) || 'Unknown',
1249
+ );
1250
+ }
1251
+ }
1252
+ return current;
1253
+ },
1254
+ set current(value) {
1255
+ if (currentlyRenderingFiber !== null && !didWarnAboutWrite) {
1256
+ if (
1257
+ hasBeenInitialized ||
1258
+ (!hasBeenInitialized && !didCheckForLazyInit)
1259
+ ) {
1260
+ didWarnAboutWrite = true;
1261
+ console.warn(
1262
+ '%s: Unsafe write of a mutable value during render.\n\n' +
1263
+ 'Writing to a ref during render is only safe if the ref holds ' +
1264
+ 'a lazily-initialized value that is only set once.\n',
1265
+ getComponentName(currentlyRenderingFiber.type) || 'Unknown',
1266
+ );
1267
+ }
1268
+ }
1269
+
1270
+ hasBeenInitialized = true;
1271
+ current = value;
1272
+ },
1273
+ };
1274
+ Object.seal(ref);
1275
+ hook.memoizedState = ref;
1276
+ return ref;
1277
+ } else {
1278
+ const ref = {current: initialValue};
1279
+ hook.memoizedState = ref;
1280
+ return ref;
1281
+ }
1282
+ } else {
1283
+ const ref = {current: initialValue};
1284
+ hook.memoizedState = ref;
1285
+ return ref;
1286
}
1206
- hook.memoizedState = ref;
1207
- return ref;
1287
}
1288
1289
function updateRef<T>(initialValue: T): {|current: T|} {
@@ -1591,24 +1670,24 @@ function startTransition(setPending, callback) {
1670
1671
function mountTransition(): [(() => void) => void, boolean] {
1672
const [isPending, setPending] = mountState(false);
1594
- // The `start` method can be stored on a ref, since `setPending`
1595
- // never changes.
1673
+ // The `start` method never changes.
1674
const start = startTransition.bind(null, setPending);
1597
- mountRef(start);
1675
+ const hook = mountWorkInProgressHook();
1676
+ hook.memoizedState = start;
1677
return [start, isPending];
1678
}
1679
1680
function updateTransition(): [(() => void) => void, boolean] {
1681
const [isPending] = updateState(false);
1603
- const startRef = updateRef();
1604
- const start: (() => void) => void = (startRef.current: any);
1682
+ const hook = updateWorkInProgressHook();
1683
+ const start = hook.memoizedState;
1684
return [start, isPending];
1685
}
1686
1687
function rerenderTransition(): [(() => void) => void, boolean] {
1688
const [isPending] = rerenderState(false);
1610
- const startRef = updateRef();
1611
- const start: (() => void) => void = (startRef.current: any);
1689
+ const hook = updateWorkInProgressHook();
1690
+ const start = hook.memoizedState;
1691
return [start, isPending];
1692
}
1693
packages/react-reconciler/src/ReactFiberHooks.old.js
+91
-12
@@ -26,6 +26,7 @@ import {
26
enableSchedulingProfiler,
27
enableNewReconciler,
28
decoupleUpdatePriorityFromScheduler,
29
+ enableUseRefAccessWarning,
30
} from 'shared/ReactFeatureFlags';
31
32
import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
@@ -1175,14 +1176,92 @@ function pushEffect(tag, create, destroy, deps) {
1176
return effect;
1177
}
1178
1179
+let stackContainsErrorMessage: boolean | null = null;
1180
+
1181
+function getCallerStackFrame(): string {
1182
+ const stackFrames = new Error('Error message').stack.split('\n');
1183
+
1184
+ // Some browsers (e.g. Chrome) include the error message in the stack
1185
+ // but others (e.g. Firefox) do not.
1186
+ if (stackContainsErrorMessage === null) {
1187
+ stackContainsErrorMessage = stackFrames[0].includes('Error message');
1188
+ }
1189
+
1190
+ return stackContainsErrorMessage
1191
+ ? stackFrames.slice(3, 4).join('\n')
1192
+ : stackFrames.slice(2, 3).join('\n');
1193
+}
1194
+
1195
function mountRef<T>(initialValue: T): {|current: T|} {
1196
const hook = mountWorkInProgressHook();
1180
- const ref = {current: initialValue};
1181
- if (__DEV__) {
1182
- Object.seal(ref);
1197
+ if (enableUseRefAccessWarning) {
1198
+ if (__DEV__) {
1199
+ // Support lazy initialization pattern shown in docs.
1200
+ // We need to store the caller stack frame so that we don't warn on subsequent renders.
1201
+ let hasBeenInitialized = initialValue != null;
1202
+ let lazyInitGetterStack = null;
1203
+ let didCheckForLazyInit = false;
1204
+
1205
+ // Only warn once per component+hook.
1206
+ let didWarnAboutRead = false;
1207
+ let didWarnAboutWrite = false;
1208
+
1209
+ let current = initialValue;
1210
+ const ref = {
1211
+ get current() {
1212
+ if (!hasBeenInitialized) {
1213
+ didCheckForLazyInit = true;
1214
+ lazyInitGetterStack = getCallerStackFrame();
1215
+ } else if (currentlyRenderingFiber !== null && !didWarnAboutRead) {
1216
+ if (
1217
+ lazyInitGetterStack === null ||
1218
+ lazyInitGetterStack !== getCallerStackFrame()
1219
+ ) {
1220
+ didWarnAboutRead = true;
1221
+ console.warn(
1222
+ '%s: Unsafe read of a mutable value during render.\n\n' +
1223
+ 'Reading from a ref during render is only safe if:\n' +
1224
+ '1. The ref value has not been updated, or\n' +
1225
+ '2. The ref holds a lazily-initialized value that is only set once.\n',
1226
+ getComponentName(currentlyRenderingFiber.type) || 'Unknown',
1227
+ );
1228
+ }
1229
+ }
1230
+ return current;
1231
+ },
1232
+ set current(value) {
1233
+ if (currentlyRenderingFiber !== null && !didWarnAboutWrite) {
1234
+ if (
1235
+ hasBeenInitialized ||
1236
+ (!hasBeenInitialized && !didCheckForLazyInit)
1237
+ ) {
1238
+ didWarnAboutWrite = true;
1239
+ console.warn(
1240
+ '%s: Unsafe write of a mutable value during render.\n\n' +
1241
+ 'Writing to a ref during render is only safe if the ref holds ' +
1242
+ 'a lazily-initialized value that is only set once.\n',
1243
+ getComponentName(currentlyRenderingFiber.type) || 'Unknown',
1244
+ );
1245
+ }
1246
+ }
1247
+
1248
+ hasBeenInitialized = true;
1249
+ current = value;
1250
+ },
1251
+ };
1252
+ Object.seal(ref);
1253
+ hook.memoizedState = ref;
1254
+ return ref;
1255
+ } else {
1256
+ const ref = {current: initialValue};
1257
+ hook.memoizedState = ref;
1258
+ return ref;
1259
+ }
1260
+ } else {
1261
+ const ref = {current: initialValue};
1262
+ hook.memoizedState = ref;
1263
+ return ref;
1264
}
1184
- hook.memoizedState = ref;
1185
- return ref;
1265
}
1266
1267
function updateRef<T>(initialValue: T): {|current: T|} {
@@ -1534,24 +1613,24 @@ function startTransition(setPending, callback) {
1613
1614
function mountTransition(): [(() => void) => void, boolean] {
1615
const [isPending, setPending] = mountState(false);
1537
- // The `start` method can be stored on a ref, since `setPending`
1538
- // never changes.
1616
+ // The `start` method never changes.
1617
const start = startTransition.bind(null, setPending);
1540
- mountRef(start);
1618
+ const hook = mountWorkInProgressHook();
1619
+ hook.memoizedState = start;
1620
return [start, isPending];
1621
}
1622
1623
function updateTransition(): [(() => void) => void, boolean] {
1624
const [isPending] = updateState(false);
1546
- const startRef = updateRef();
1547
- const start: (() => void) => void = (startRef.current: any);
1625
+ const hook = updateWorkInProgressHook();
1626
+ const start = hook.memoizedState;
1627
return [start, isPending];
1628
}
1629
1630
function rerenderTransition(): [(() => void) => void, boolean] {
1631
const [isPending] = rerenderState(false);
1553
- const startRef = updateRef();
1554
- const start: (() => void) => void = (startRef.current: any);
1632
+ const hook = updateWorkInProgressHook();
1633
+ const start = hook.memoizedState;
1634
return [start, isPending];
1635
}
1636
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+2
-87
@@ -1536,7 +1536,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1536
it('does not show a warning when a component updates a childs state from within passive unmount function', () => {
1537
function Parent() {
1538
Scheduler.unstable_yieldValue('Parent');
1539
- const updaterRef = React.useRef(null);
1539
+ const updaterRef = useRef(null);
1540
React.useEffect(() => {
1541
Scheduler.unstable_yieldValue('Parent passive create');
1542
return () => {
@@ -2612,7 +2612,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2612
});
2613
2614
// @gate new
2615
- it('should skip unmounted boundaries and use the nearest still-mounted boundary', () => {
2615
+ it('should skip unmounted boundaries and use the nearest still-mounted boundary', () => {
2616
function Conditional({showChildren}) {
2617
if (showChildren) {
2618
return (
@@ -3202,91 +3202,6 @@ describe('ReactHooksWithNoopRenderer', () => {
3202
});
3203
});
3204
3205
- describe('useRef', () => {
3206
- it('creates a ref object initialized with the provided value', () => {
3207
- jest.useFakeTimers();
3208
-
3209
- function useDebouncedCallback(callback, ms, inputs) {
3210
- const timeoutID = useRef(-1);
3211
- useEffect(() => {
3212
- return function unmount() {
3213
- clearTimeout(timeoutID.current);
3214
- };
3215
- }, []);
3216
- const debouncedCallback = useCallback(
3217
- (...args) => {
3218
- clearTimeout(timeoutID.current);
3219
- timeoutID.current = setTimeout(callback, ms, ...args);
3220
- },
3221
- [callback, ms],
3222
- );
3223
- return useCallback(debouncedCallback, inputs);
3224
- }
3225
-
3226
- let ping;
3227
- function App() {
3228
- ping = useDebouncedCallback(
3229
- value => {
3230
- Scheduler.unstable_yieldValue('ping: ' + value);
3231
- },
3232
- 100,
3233
- [],
3234
- );
3235
- return null;
3236
- }
3237
-
3238
- act(() => {
3239
- ReactNoop.render(<App />);
3240
- });
3241
- expect(Scheduler).toHaveYielded([]);
3242
-
3243
- ping(1);
3244
- ping(2);
3245
- ping(3);
3246
-
3247
- expect(Scheduler).toHaveYielded([]);
3248
-
3249
- jest.advanceTimersByTime(100);
3250
-
3251
- expect(Scheduler).toHaveYielded(['ping: 3']);
3252
-
3253
- ping(4);
3254
- jest.advanceTimersByTime(20);
3255
- ping(5);
3256
- ping(6);
3257
- jest.advanceTimersByTime(80);
3258
-
3259
- expect(Scheduler).toHaveYielded([]);
3260
-
3261
- jest.advanceTimersByTime(20);
3262
- expect(Scheduler).toHaveYielded(['ping: 6']);
3263
- });
3264
-
3265
- it('should return the same ref during re-renders', () => {
3266
- function Counter() {
3267
- const ref = useRef('val');
3268
- const [count, setCount] = useState(0);
3269
- const [firstRef] = useState(ref);
3270
-
3271
- if (firstRef !== ref) {
3272
- throw new Error('should never change');
3273
- }
3274
-
3275
- if (count < 3) {
3276
- setCount(count + 1);
3277
- }
3278
-
3279
- return <Text text={ref.current} />;
3280
- }
3281
-
3282
- ReactNoop.render(<Counter />);
3283
- expect(Scheduler).toFlushAndYield(['val']);
3284
-
3285
- ReactNoop.render(<Counter />);
3286
- expect(Scheduler).toFlushAndYield(['val']);
3287
- });
3288
- });
3289
-
3205
describe('useImperativeHandle', () => {
3206
it('does not update when deps are the same', () => {
3207
const INCREMENT = 'INCREMENT';
packages/react-reconciler/src/__tests__/useRef-test.internal.js
new
+364
@@ -0,0 +1,364 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ * @jest-environment node
9
+ */
10
+
11
+/* eslint-disable no-func-assign */
12
+
13
+'use strict';
14
+
15
+describe('useRef', () => {
16
+ let React;
17
+ let ReactNoop;
18
+ let Scheduler;
19
+ let act;
20
+ let useCallback;
21
+ let useEffect;
22
+ let useLayoutEffect;
23
+ let useRef;
24
+ let useState;
25
+
26
+ beforeEach(() => {
27
+ React = require('react');
28
+ ReactNoop = require('react-noop-renderer');
29
+ Scheduler = require('scheduler');
30
+
31
+ const ReactFeatureFlags = require('shared/ReactFeatureFlags');
32
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
33
+
34
+ act = ReactNoop.act;
35
+ useCallback = React.useCallback;
36
+ useEffect = React.useEffect;
37
+ useLayoutEffect = React.useLayoutEffect;
38
+ useRef = React.useRef;
39
+ useState = React.useState;
40
+ });
41
+
42
+ function Text(props) {
43
+ Scheduler.unstable_yieldValue(props.text);
44
+ return <span prop={props.text} />;
45
+ }
46
+
47
+ it('creates a ref object initialized with the provided value', () => {
48
+ jest.useFakeTimers();
49
+
50
+ function useDebouncedCallback(callback, ms, inputs) {
51
+ const timeoutID = useRef(-1);
52
+ useEffect(() => {
53
+ return function unmount() {
54
+ clearTimeout(timeoutID.current);
55
+ };
56
+ }, []);
57
+ const debouncedCallback = useCallback(
58
+ (...args) => {
59
+ clearTimeout(timeoutID.current);
60
+ timeoutID.current = setTimeout(callback, ms, ...args);
61
+ },
62
+ [callback, ms],
63
+ );
64
+ return useCallback(debouncedCallback, inputs);
65
+ }
66
+
67
+ let ping;
68
+ function App() {
69
+ ping = useDebouncedCallback(
70
+ value => {
71
+ Scheduler.unstable_yieldValue('ping: ' + value);
72
+ },
73
+ 100,
74
+ [],
75
+ );
76
+ return null;
77
+ }
78
+
79
+ act(() => {
80
+ ReactNoop.render(<App />);
81
+ });
82
+ expect(Scheduler).toHaveYielded([]);
83
+
84
+ ping(1);
85
+ ping(2);
86
+ ping(3);
87
+
88
+ expect(Scheduler).toHaveYielded([]);
89
+
90
+ jest.advanceTimersByTime(100);
91
+
92
+ expect(Scheduler).toHaveYielded(['ping: 3']);
93
+
94
+ ping(4);
95
+ jest.advanceTimersByTime(20);
96
+ ping(5);
97
+ ping(6);
98
+ jest.advanceTimersByTime(80);
99
+
100
+ expect(Scheduler).toHaveYielded([]);
101
+
102
+ jest.advanceTimersByTime(20);
103
+ expect(Scheduler).toHaveYielded(['ping: 6']);
104
+ });
105
+
106
+ it('should return the same ref during re-renders', () => {
107
+ function Counter() {
108
+ const ref = useRef('val');
109
+ const [count, setCount] = useState(0);
110
+ const [firstRef] = useState(ref);
111
+
112
+ if (firstRef !== ref) {
113
+ throw new Error('should never change');
114
+ }
115
+
116
+ if (count < 3) {
117
+ setCount(count + 1);
118
+ }
119
+
120
+ return <Text text={count} />;
121
+ }
122
+
123
+ ReactNoop.render(<Counter />);
124
+ expect(Scheduler).toFlushAndYield([3]);
125
+
126
+ ReactNoop.render(<Counter />);
127
+ expect(Scheduler).toFlushAndYield([3]);
128
+ });
129
+
130
+ if (__DEV__) {
131
+ it('should never warn when attaching to children', () => {
132
+ class Component extends React.Component {
133
+ render() {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ function Example({phase}) {
139
+ const hostRef = useRef();
140
+ const classRef = useRef();
141
+ return (
142
+ <>
143
+ <div key={`host-${phase}`} ref={hostRef} />
144
+ <Component key={`class-${phase}`} ref={classRef} />
145
+ </>
146
+ );
147
+ }
148
+
149
+ act(() => {
150
+ ReactNoop.render(<Example phase="mount" />);
151
+ });
152
+ act(() => {
153
+ ReactNoop.render(<Example phase="update" />);
154
+ });
155
+ });
156
+
157
+ // @gate enableUseRefAccessWarning
158
+ it('should warn about reads during render', () => {
159
+ function Example() {
160
+ const ref = useRef(123);
161
+ let value;
162
+ expect(() => {
163
+ value = ref.current;
164
+ }).toWarnDev([
165
+ 'Example: Unsafe read of a mutable value during render.',
166
+ ]);
167
+ return value;
168
+ }
169
+
170
+ act(() => {
171
+ ReactNoop.render(<Example />);
172
+ });
173
+ });
174
+
175
+ it('should not warn about lazy init during render', () => {
176
+ function Example() {
177
+ const ref1 = useRef(null);
178
+ const ref2 = useRef(undefined);
179
+ // Read: safe because lazy init:
180
+ if (ref1.current === null) {
181
+ ref1.current = 123;
182
+ }
183
+ if (ref2.current === undefined) {
184
+ ref2.current = 123;
185
+ }
186
+ return null;
187
+ }
188
+
189
+ act(() => {
190
+ ReactNoop.render(<Example />);
191
+ });
192
+
193
+ // Should not warn after an update either.
194
+ act(() => {
195
+ ReactNoop.render(<Example />);
196
+ });
197
+ });
198
+
199
+ it('should not warn about lazy init outside of render', () => {
200
+ function Example() {
201
+ // eslint-disable-next-line no-unused-vars
202
+ const [didMount, setDidMount] = useState(false);
203
+ const ref1 = useRef(null);
204
+ const ref2 = useRef(undefined);
205
+ useLayoutEffect(() => {
206
+ ref1.current = 123;
207
+ ref2.current = 123;
208
+ setDidMount(true);
209
+ }, []);
210
+ return null;
211
+ }
212
+
213
+ act(() => {
214
+ ReactNoop.render(<Example />);
215
+ });
216
+ });
217
+
218
+ // @gate enableUseRefAccessWarning
219
+ it('should warn about unconditional lazy init during render', () => {
220
+ function Example() {
221
+ const ref1 = useRef(null);
222
+ const ref2 = useRef(undefined);
223
+
224
+ if (shouldExpectWarning) {
225
+ expect(() => {
226
+ ref1.current = 123;
227
+ }).toWarnDev([
228
+ 'Example: Unsafe write of a mutable value during render',
229
+ ]);
230
+ expect(() => {
231
+ ref2.current = 123;
232
+ }).toWarnDev([
233
+ 'Example: Unsafe write of a mutable value during render',
234
+ ]);
235
+ } else {
236
+ ref1.current = 123;
237
+ ref1.current = 123;
238
+ }
239
+
240
+ // But only warn once
241
+ ref1.current = 345;
242
+ ref1.current = 345;
243
+
244
+ return null;
245
+ }
246
+
247
+ let shouldExpectWarning = true;
248
+ act(() => {
249
+ ReactNoop.render(<Example />);
250
+ });
251
+
252
+ // Should not warn again on update.
253
+ shouldExpectWarning = false;
254
+ act(() => {
255
+ ReactNoop.render(<Example />);
256
+ });
257
+ });
258
+
259
+ // @gate enableUseRefAccessWarning
260
+ it('should warn about reads to ref after lazy init pattern', () => {
261
+ function Example() {
262
+ const ref1 = useRef(null);
263
+ const ref2 = useRef(undefined);
264
+
265
+ // Read 1: safe because lazy init:
266
+ if (ref1.current === null) {
267
+ ref1.current = 123;
268
+ }
269
+ if (ref2.current === undefined) {
270
+ ref2.current = 123;
271
+ }
272
+
273
+ let value;
274
+ expect(() => {
275
+ value = ref1.current;
276
+ }).toWarnDev(['Example: Unsafe read of a mutable value during render']);
277
+ expect(() => {
278
+ value = ref2.current;
279
+ }).toWarnDev(['Example: Unsafe read of a mutable value during render']);
280
+
281
+ // But it should only warn once.
282
+ value = ref1.current;
283
+ value = ref2.current;
284
+
285
+ return value;
286
+ }
287
+
288
+ act(() => {
289
+ ReactNoop.render(<Example />);
290
+ });
291
+ });
292
+
293
+ // @gate enableUseRefAccessWarning
294
+ it('should warn about writes to ref after lazy init pattern', () => {
295
+ function Example() {
296
+ const ref1 = useRef(null);
297
+ const ref2 = useRef(undefined);
298
+ // Read: safe because lazy init:
299
+ if (ref1.current === null) {
300
+ ref1.current = 123;
301
+ }
302
+ if (ref2.current === undefined) {
303
+ ref2.current = 123;
304
+ }
305
+
306
+ expect(() => {
307
+ ref1.current = 456;
308
+ }).toWarnDev([
309
+ 'Example: Unsafe write of a mutable value during render',
310
+ ]);
311
+ expect(() => {
312
+ ref2.current = 456;
313
+ }).toWarnDev([
314
+ 'Example: Unsafe write of a mutable value during render',
315
+ ]);
316
+
317
+ return null;
318
+ }
319
+
320
+ act(() => {
321
+ ReactNoop.render(<Example />);
322
+ });
323
+ });
324
+
325
+ it('should not warn about reads or writes within effect', () => {
326
+ function Example() {
327
+ const ref = useRef(123);
328
+ useLayoutEffect(() => {
329
+ expect(ref.current).toBe(123);
330
+ ref.current = 456;
331
+ expect(ref.current).toBe(456);
332
+ }, []);
333
+ useEffect(() => {
334
+ expect(ref.current).toBe(456);
335
+ ref.current = 789;
336
+ expect(ref.current).toBe(789);
337
+ }, []);
338
+ return null;
339
+ }
340
+
341
+ act(() => {
342
+ ReactNoop.render(<Example />);
343
+ });
344
+
345
+ ReactNoop.flushPassiveEffects();
346
+ });
347
+
348
+ it('should not warn about reads or writes outside of render phase (e.g. event handler)', () => {
349
+ let ref;
350
+ function Example() {
351
+ ref = useRef(123);
352
+ return null;
353
+ }
354
+
355
+ act(() => {
356
+ ReactNoop.render(<Example />);
357
+ });
358
+
359
+ expect(ref.current).toBe(123);
360
+ ref.current = 456;
361
+ expect(ref.current).toBe(456);
362
+ });
363
+ }
364
+});
packages/shared/ReactFeatureFlags.js
+2
@@ -134,3 +134,5 @@ export const decoupleUpdatePriorityFromScheduler = false;
134
export const enableDiscreteEventFlushingChange = false;
135
136
export const enableDoubleInvokingEffects = false;
137
+
138
+export const enableUseRefAccessWarning = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -51,6 +51,7 @@ export const decoupleUpdatePriorityFromScheduler = false;
51
export const enableDiscreteEventFlushingChange = false;
52
53
export const enableDoubleInvokingEffects = false;
54
+export const enableUseRefAccessWarning = false;
55
56
// Flow magic to verify the exports of this file match the original version.
57
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -50,6 +50,7 @@ export const decoupleUpdatePriorityFromScheduler = false;
50
export const enableDiscreteEventFlushingChange = false;
51
52
export const enableDoubleInvokingEffects = false;
53
+export const enableUseRefAccessWarning = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -50,6 +50,7 @@ export const decoupleUpdatePriorityFromScheduler = false;
50
export const enableDiscreteEventFlushingChange = false;
51
52
export const enableDoubleInvokingEffects = false;
53
+export const enableUseRefAccessWarning = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -50,6 +50,7 @@ export const decoupleUpdatePriorityFromScheduler = false;
50
export const enableDiscreteEventFlushingChange = false;
51
52
export const enableDoubleInvokingEffects = false;
53
+export const enableUseRefAccessWarning = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -50,6 +50,7 @@ export const decoupleUpdatePriorityFromScheduler = false;
50
export const enableDiscreteEventFlushingChange = false;
51
52
export const enableDoubleInvokingEffects = false;
53
+export const enableUseRefAccessWarning = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -50,6 +50,7 @@ export const decoupleUpdatePriorityFromScheduler = false;
50
export const enableDiscreteEventFlushingChange = false;
51
52
export const enableDoubleInvokingEffects = false;
53
+export const enableUseRefAccessWarning = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -50,6 +50,7 @@ export const decoupleUpdatePriorityFromScheduler = false;
50
export const enableDiscreteEventFlushingChange = true;
51
52
export const enableDoubleInvokingEffects = false;
53
+export const enableUseRefAccessWarning = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -48,3 +48,4 @@ export const enableTrustedTypesIntegration = false;
48
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
49
50
export const enableDoubleInvokingEffects = false;
51
+export const enableUseRefAccessWarning = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -28,6 +28,7 @@ export const {
28
enableDebugTracing,
29
skipUnmountedBoundaries,
30
enableDoubleInvokingEffects,
31
+ enableUseRefAccessWarning,
32
} = dynamicFeatureFlags;
33
34
// On WWW, __EXPERIMENTAL__ is used for a new modern build.