@samitouri / QOS-React / commits / c59c3dfe55

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.