Make all readContext() and Hook-in-a-Hook checks DEV-only (#14677)
* Make readContext() in Hooks DEV-only warning * Warn about readContext() during class render-phase setState() * Warn on readContext() in SSR inside useMemo and useReducer * Make all Hooks-in-Hooks warnings DEV-only * Rename stashContextDependencies * Clean up warning state on errors
Dan Abramov committed
Jan 24, 2019 at 19:31 UTC
8bcc88f2e7a7dc3e950bfcca1d3c6bbd251c4b20
7 files changed
+388
-129
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js
+70
-41
@@ -419,50 +419,47 @@ describe('ReactDOMServerHooks', () => {
419
},
420
);
421
422
- itThrowsWhenRendering(
423
- 'a hook inside useMemo',
424
- async render => {
425
- function App() {
426
- useMemo(() => {
427
- useState();
428
- return 0;
429
- });
430
- return null;
431
- }
432
- return render(<App />);
433
- },
434
- 'Hooks can only be called inside the body of a function component.',
435
- );
422
+ itRenders('with a warning for useState inside useMemo', async render => {
423
+ function App() {
424
+ useMemo(() => {
425
+ useState();
426
+ return 0;
427
+ });
428
+ return 'hi';
429
+ }
430
437
- itThrowsWhenRendering(
438
- 'a hook inside useReducer',
439
- async render => {
440
- function App() {
441
- const [value, dispatch] = useReducer((state, action) => {
442
- useRef(0);
443
- return state;
444
- }, 0);
445
- dispatch('foo');
446
- return value;
447
- }
448
- return render(<App />);
449
- },
450
- 'Hooks can only be called inside the body of a function component.',
451
- );
431
+ const domNode = await render(<App />, 1);
432
+ expect(domNode.textContent).toEqual('hi');
433
+ });
434
453
- itThrowsWhenRendering(
454
- 'a hook inside useState',
455
- async render => {
456
- function App() {
457
- useState(() => {
458
- useRef(0);
459
- return 0;
460
- });
435
+ itRenders('with a warning for useRef inside useReducer', async render => {
436
+ function App() {
437
+ const [value, dispatch] = useReducer((state, action) => {
438
+ useRef(0);
439
+ return state + 1;
440
+ }, 0);
441
+ if (value === 0) {
442
+ dispatch();
443
}
462
- return render(<App />);
463
- },
464
- 'Hooks can only be called inside the body of a function component.',
465
- );
444
+ return value;
445
+ }
446
+
447
+ const domNode = await render(<App />, 1);
448
+ expect(domNode.textContent).toEqual('1');
449
+ });
450
+
451
+ itRenders('with a warning for useRef inside useState', async render => {
452
+ function App() {
453
+ const [value] = useState(() => {
454
+ useRef(0);
455
+ return 0;
456
+ });
457
+ return value;
458
+ }
459
+
460
+ const domNode = await render(<App />, 1);
461
+ expect(domNode.textContent).toEqual('0');
462
+ });
463
});
464
465
describe('useRef', () => {
@@ -716,4 +713,36 @@ describe('ReactDOMServerHooks', () => {
713
expect(domNode.textContent).toEqual('undefined');
714
});
715
});
716
+
717
+ describe('readContext', () => {
718
+ function readContext(Context, observedBits) {
719
+ const dispatcher =
720
+ React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
721
+ .ReactCurrentDispatcher.current;
722
+ return dispatcher.readContext(Context, observedBits);
723
+ }
724
+
725
+ itRenders('with a warning inside useMemo and useReducer', async render => {
726
+ const Context = React.createContext(42);
727
+
728
+ function ReadInMemo(props) {
729
+ let count = React.useMemo(() => readContext(Context), []);
730
+ return <Text text={count} />;
731
+ }
732
+
733
+ function ReadInReducer(props) {
734
+ let [count, dispatch] = React.useReducer(() => readContext(Context));
735
+ if (count !== 42) {
736
+ dispatch();
737
+ }
738
+ return <Text text={count} />;
739
+ }
740
+
741
+ const domNode1 = await render(<ReadInMemo />, 1);
742
+ expect(domNode1.textContent).toEqual('42');
743
+
744
+ const domNode2 = await render(<ReadInReducer />, 1);
745
+ expect(domNode2.textContent).toEqual('42');
746
+ });
747
+ });
748
});
packages/react-dom/src/server/ReactPartialRendererHooks.js
+45
-10
@@ -49,6 +49,8 @@ let renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null = null;
49
let numberOfReRenders: number = 0;
50
const RE_RENDER_LIMIT = 25;
51
52
+let isInHookUserCodeInDev = false;
53
+
54
// In DEV, this is the name of the currently executing primitive hook
55
let currentHookNameInDev: ?string;
56
@@ -57,6 +59,14 @@ function resolveCurrentlyRenderingComponent(): Object {
59
currentlyRenderingComponent !== null,
60
'Hooks can only be called inside the body of a function component.',
61
);
62
+ if (__DEV__) {
63
+ warning(
64
+ !isInHookUserCodeInDev,
65
+ 'Hooks can only be called inside the body of a function component. ' +
66
+ 'Do not call Hooks inside other Hooks. For more information, see ' +
67
+ 'https://fb.me/rules-of-hooks',
68
+ );
69
+ }
70
return currentlyRenderingComponent;
71
}
72
@@ -137,6 +147,9 @@ function createWorkInProgressHook(): Hook {
147
148
export function prepareToUseHooks(componentIdentity: Object): void {
149
currentlyRenderingComponent = componentIdentity;
150
+ if (__DEV__) {
151
+ isInHookUserCodeInDev = false;
152
+ }
153
154
// The following should have already been reset
155
// didScheduleRenderPhaseUpdate = false;
@@ -173,6 +186,9 @@ export function finishHooks(
186
numberOfReRenders = 0;
187
renderPhaseUpdates = null;
188
workInProgressHook = null;
189
+ if (__DEV__) {
190
+ isInHookUserCodeInDev = false;
191
+ }
192
193
// These were reset above
194
// currentlyRenderingComponent = null;
@@ -191,6 +207,15 @@ function readContext<T>(
207
): T {
208
let threadID = currentThreadID;
209
validateContextBounds(context, threadID);
210
+ if (__DEV__) {
211
+ warning(
212
+ !isInHookUserCodeInDev,
213
+ 'Context can only be read while React is rendering. ' +
214
+ 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
215
+ 'In function components, you can read it directly in the function body, but not ' +
216
+ 'inside Hooks like useReducer() or useMemo().',
217
+ );
218
+ }
219
return context[threadID];
220
}
221
@@ -234,7 +259,7 @@ export function useReducer<S, A>(
259
currentHookNameInDev = 'useReducer';
260
}
261
}
237
- let component = (currentlyRenderingComponent = resolveCurrentlyRenderingComponent());
262
+ currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
263
workInProgressHook = createWorkInProgressHook();
264
if (isReRender) {
265
// This is a re-render. Apply the new render phase updates to the previous
@@ -253,10 +278,13 @@ export function useReducer<S, A>(
278
// priority because it will always be the same as the current
279
// render's.
280
const action = update.action;
256
- // Temporarily clear to forbid calling Hooks.
257
- currentlyRenderingComponent = null;
281
+ if (__DEV__) {
282
+ isInHookUserCodeInDev = true;
283
+ }
284
newState = reducer(newState, action);
259
- currentlyRenderingComponent = component;
285
+ if (__DEV__) {
286
+ isInHookUserCodeInDev = false;
287
+ }
288
update = update.next;
289
} while (update !== null);
290
@@ -267,7 +295,9 @@ export function useReducer<S, A>(
295
}
296
return [workInProgressHook.memoizedState, dispatch];
297
} else {
270
- currentlyRenderingComponent = null;
298
+ if (__DEV__) {
299
+ isInHookUserCodeInDev = true;
300
+ }
301
if (reducer === basicStateReducer) {
302
// Special case for `useState`.
303
if (typeof initialState === 'function') {
@@ -276,7 +306,9 @@ export function useReducer<S, A>(
306
} else if (initialAction !== undefined && initialAction !== null) {
307
initialState = reducer(initialState, initialAction);
308
}
279
- currentlyRenderingComponent = component;
309
+ if (__DEV__) {
310
+ isInHookUserCodeInDev = false;
311
+ }
312
workInProgressHook.memoizedState = initialState;
313
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
314
last: null,
@@ -292,7 +324,7 @@ export function useReducer<S, A>(
324
}
325
326
function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
295
- let component = (currentlyRenderingComponent = resolveCurrentlyRenderingComponent());
327
+ currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
328
workInProgressHook = createWorkInProgressHook();
329
330
const nextDeps = deps === undefined ? null : deps;
@@ -309,10 +341,13 @@ function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
341
}
342
}
343
312
- // Temporarily clear to forbid calling Hooks.
313
- currentlyRenderingComponent = null;
344
+ if (__DEV__) {
345
+ isInHookUserCodeInDev = true;
346
+ }
347
const nextValue = nextCreate();
315
- currentlyRenderingComponent = component;
348
+ if (__DEV__) {
349
+ isInHookUserCodeInDev = false;
350
+ }
351
workInProgressHook.memoizedState = [nextValue, nextDeps];
352
return nextValue;
353
}
packages/react-reconciler/src/ReactFiberHooks.js
+68
-30
@@ -15,8 +15,8 @@ import type {HookEffectTag} from './ReactHookEffectTags';
15
import {NoWork} from './ReactFiberExpirationTime';
16
import {
17
readContext,
18
- stashContextDependencies,
19
- unstashContextDependencies,
18
+ enterDisallowedContextReadInDEV,
19
+ exitDisallowedContextReadInDEV,
20
} from './ReactFiberNewContext';
21
import {
22
Update as UpdateEffect,
@@ -72,6 +72,7 @@ type HookType =
72
// the first instance of a hook mismatch in a component,
73
// represented by a portion of it's stacktrace
74
let currentHookMismatchInDev = null;
75
+let isInHookUserCodeInDev = false;
76
77
let didWarnAboutMismatchedHooksForComponent;
78
if (__DEV__) {
@@ -153,6 +154,17 @@ function resolveCurrentlyRenderingFiber(): Fiber {
154
currentlyRenderingFiber !== null,
155
'Hooks can only be called inside the body of a function component.',
156
);
157
+ if (__DEV__) {
158
+ // Check if we're inside Hooks like useMemo(). DEV-only for perf.
159
+ // TODO: we can make a better warning message with currentHookNameInDev
160
+ // if we also make sure it's consistently assigned in the right order.
161
+ warning(
162
+ !isInHookUserCodeInDev,
163
+ 'Hooks can only be called inside the body of a function component. ' +
164
+ 'Do not call Hooks inside other Hooks. For more information, see ' +
165
+ 'https://fb.me/rules-of-hooks',
166
+ );
167
+ }
168
return currentlyRenderingFiber;
169
}
170
@@ -299,6 +311,11 @@ export function renderWithHooks(
311
currentlyRenderingFiber = workInProgress;
312
firstCurrentHook = current !== null ? current.memoizedState : null;
313
314
+ if (__DEV__) {
315
+ exitDisallowedContextReadInDEV();
316
+ isInHookUserCodeInDev = false;
317
+ }
318
+
319
// The following should have already been reset
320
// currentHook = null;
321
// workInProgressHook = null;
@@ -395,6 +412,8 @@ export function bailoutHooks(
412
export function resetHooks(): void {
413
if (__DEV__) {
414
flushHookMismatchWarnings();
415
+ exitDisallowedContextReadInDEV();
416
+ isInHookUserCodeInDev = false;
417
}
418
419
// This is used to reset the state of this module when a component throws.
@@ -573,7 +592,7 @@ export function useReducer<S, A>(
592
currentHookNameInDev = 'useReducer';
593
}
594
}
576
- let fiber = (currentlyRenderingFiber = resolveCurrentlyRenderingFiber());
595
+ currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
596
workInProgressHook = createWorkInProgressHook();
597
if (__DEV__) {
598
currentHookNameInDev = null;
@@ -597,12 +616,15 @@ export function useReducer<S, A>(
616
// priority because it will always be the same as the current
617
// render's.
618
const action = update.action;
600
- // Temporarily clear to forbid calling Hooks in a reducer.
601
- currentlyRenderingFiber = null;
602
- stashContextDependencies();
619
+ if (__DEV__) {
620
+ enterDisallowedContextReadInDEV();
621
+ isInHookUserCodeInDev = true;
622
+ }
623
newState = reducer(newState, action);
604
- currentlyRenderingFiber = fiber;
605
- unstashContextDependencies();
624
+ if (__DEV__) {
625
+ exitDisallowedContextReadInDEV();
626
+ isInHookUserCodeInDev = false;
627
+ }
628
update = update.next;
629
} while (update !== null);
630
@@ -671,12 +693,15 @@ export function useReducer<S, A>(
693
newState = ((update.eagerState: any): S);
694
} else {
695
const action = update.action;
674
- // Temporarily clear to forbid calling Hooks in a reducer.
675
- currentlyRenderingFiber = null;
676
- stashContextDependencies();
696
+ if (__DEV__) {
697
+ enterDisallowedContextReadInDEV();
698
+ isInHookUserCodeInDev = true;
699
+ }
700
newState = reducer(newState, action);
678
- currentlyRenderingFiber = fiber;
679
- unstashContextDependencies();
701
+ if (__DEV__) {
702
+ exitDisallowedContextReadInDEV();
703
+ isInHookUserCodeInDev = false;
704
+ }
705
}
706
}
707
prevUpdate = update;
@@ -705,9 +730,10 @@ export function useReducer<S, A>(
730
const dispatch: Dispatch<A> = (queue.dispatch: any);
731
return [workInProgressHook.memoizedState, dispatch];
732
}
708
- // Temporarily clear to forbid calling Hooks in a reducer.
709
- currentlyRenderingFiber = null;
710
- stashContextDependencies();
733
+ if (__DEV__) {
734
+ enterDisallowedContextReadInDEV();
735
+ isInHookUserCodeInDev = true;
736
+ }
737
// There's no existing queue, so this is the initial render.
738
if (reducer === basicStateReducer) {
739
// Special case for `useState`.
@@ -717,8 +743,10 @@ export function useReducer<S, A>(
743
} else if (initialAction !== undefined && initialAction !== null) {
744
initialState = reducer(initialState, initialAction);
745
}
720
- currentlyRenderingFiber = fiber;
721
- unstashContextDependencies();
746
+ if (__DEV__) {
747
+ exitDisallowedContextReadInDEV();
748
+ isInHookUserCodeInDev = false;
749
+ }
750
workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;
751
queue = workInProgressHook.queue = {
752
last: null,
@@ -941,7 +969,7 @@ export function useMemo<T>(
969
if (__DEV__) {
970
currentHookNameInDev = 'useMemo';
971
}
944
- let fiber = (currentlyRenderingFiber = resolveCurrentlyRenderingFiber());
972
+ currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
973
workInProgressHook = createWorkInProgressHook();
974
975
const nextDeps = deps === undefined ? null : deps;
@@ -960,12 +988,15 @@ export function useMemo<T>(
988
}
989
}
990
963
- // Temporarily clear to forbid calling Hooks.
964
- currentlyRenderingFiber = null;
965
- stashContextDependencies();
991
+ if (__DEV__) {
992
+ enterDisallowedContextReadInDEV();
993
+ isInHookUserCodeInDev = true;
994
+ }
995
const nextValue = nextCreate();
967
- currentlyRenderingFiber = fiber;
968
- unstashContextDependencies();
996
+ if (__DEV__) {
997
+ exitDisallowedContextReadInDEV();
998
+ isInHookUserCodeInDev = false;
999
+ }
1000
workInProgressHook.memoizedState = [nextValue, nextDeps];
1001
if (__DEV__) {
1002
currentHookNameInDev = null;
@@ -1063,13 +1094,15 @@ function dispatchAction<S, A>(
1094
if (eagerReducer !== null) {
1095
try {
1096
const currentState: S = (queue.eagerState: any);
1066
- // Temporarily clear to forbid calling Hooks in a reducer.
1067
- let maybeFiber = currentlyRenderingFiber; // Note: likely null now unlike `fiber`
1068
- currentlyRenderingFiber = null;
1069
- stashContextDependencies();
1097
+ if (__DEV__) {
1098
+ enterDisallowedContextReadInDEV();
1099
+ isInHookUserCodeInDev = true;
1100
+ }
1101
const eagerState = eagerReducer(currentState, action);
1071
- currentlyRenderingFiber = maybeFiber;
1072
- unstashContextDependencies();
1102
+ if (__DEV__) {
1103
+ exitDisallowedContextReadInDEV();
1104
+ isInHookUserCodeInDev = false;
1105
+ }
1106
// Stash the eagerly computed state, and the reducer used to compute
1107
// it, on the update object. If the reducer hasn't changed by the
1108
// time we enter the render phase, then the eager state can be used
@@ -1085,6 +1118,11 @@ function dispatchAction<S, A>(
1118
}
1119
} catch (error) {
1120
// Suppress the error. It will throw again in the render phase.
1121
+ } finally {
1122
+ if (__DEV__) {
1123
+ exitDisallowedContextReadInDEV();
1124
+ isInHookUserCodeInDev = false;
1125
+ }
1126
}
1127
}
1128
}
packages/react-reconciler/src/ReactFiberNewContext.js
+24
-20
@@ -52,10 +52,7 @@ let currentlyRenderingFiber: Fiber | null = null;
52
let lastContextDependency: ContextDependency<mixed> | null = null;
53
let lastContextWithAllBitsObserved: ReactContext<any> | null = null;
54
55
-// We stash the variables above before entering user code in Hooks.
56
-let stashedCurrentlyRenderingFiber: Fiber | null = null;
57
-let stashedLastContextDependency: ContextDependency<mixed> | null = null;
58
-let stashedLastContextWithAllBitsObserved: ReactContext<any> | null = null;
55
+let isDisallowedContextReadInDEV: boolean = false;
56
57
export function resetContextDependences(): void {
58
// This is called right before React yields execution, to ensure `readContext`
@@ -63,26 +60,21 @@ export function resetContextDependences(): void {
60
currentlyRenderingFiber = null;
61
lastContextDependency = null;
62
lastContextWithAllBitsObserved = null;
66
-
67
- stashedCurrentlyRenderingFiber = null;
68
- stashedLastContextDependency = null;
69
- stashedLastContextWithAllBitsObserved = null;
63
+ if (__DEV__) {
64
+ isDisallowedContextReadInDEV = false;
65
+ }
66
}
67
72
-export function stashContextDependencies(): void {
73
- stashedCurrentlyRenderingFiber = currentlyRenderingFiber;
74
- stashedLastContextDependency = lastContextDependency;
75
- stashedLastContextWithAllBitsObserved = lastContextWithAllBitsObserved;
76
-
77
- currentlyRenderingFiber = null;
78
- lastContextDependency = null;
79
- lastContextWithAllBitsObserved = null;
68
+export function enterDisallowedContextReadInDEV(): void {
69
+ if (__DEV__) {
70
+ isDisallowedContextReadInDEV = true;
71
+ }
72
}
73
82
-export function unstashContextDependencies(): void {
83
- currentlyRenderingFiber = stashedCurrentlyRenderingFiber;
84
- lastContextDependency = stashedLastContextDependency;
85
- lastContextWithAllBitsObserved = stashedLastContextWithAllBitsObserved;
74
+export function exitDisallowedContextReadInDEV(): void {
75
+ if (__DEV__) {
76
+ isDisallowedContextReadInDEV = false;
77
+ }
78
}
79
80
export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
@@ -304,6 +296,18 @@ export function readContext<T>(
296
context: ReactContext<T>,
297
observedBits: void | number | boolean,
298
): T {
299
+ if (__DEV__) {
300
+ // This warning would fire if you read context inside a Hook like useMemo.
301
+ // Unlike the class check below, it's not enforced in production for perf.
302
+ warning(
303
+ !isDisallowedContextReadInDEV,
304
+ 'Context can only be read while React is rendering. ' +
305
+ 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
306
+ 'In function components, you can read it directly in the function body, but not ' +
307
+ 'inside Hooks like useReducer() or useMemo().',
308
+ );
309
+ }
310
+
311
if (lastContextWithAllBitsObserved === context) {
312
// Nothing to do. We already observe everything in this context.
313
} else if (observedBits === false || observedBits === 0) {
packages/react-reconciler/src/ReactUpdateQueue.js
+14
-1
@@ -88,6 +88,10 @@ import type {Fiber} from './ReactFiber';
88
import type {ExpirationTime} from './ReactFiberExpirationTime';
89
90
import {NoWork} from './ReactFiberExpirationTime';
91
+import {
92
+ enterDisallowedContextReadInDEV,
93
+ exitDisallowedContextReadInDEV,
94
+} from './ReactFiberNewContext';
95
import {Callback, ShouldCapture, DidCapture} from 'shared/ReactSideEffectTags';
96
import {ClassComponent} from 'shared/ReactWorkTags';
97
@@ -348,6 +352,7 @@ function getStateFromUpdate<State>(
352
if (typeof payload === 'function') {
353
// Updater function
354
if (__DEV__) {
355
+ enterDisallowedContextReadInDEV();
356
if (
357
debugRenderPhaseSideEffects ||
358
(debugRenderPhaseSideEffectsForStrictMode &&
@@ -356,7 +361,11 @@ function getStateFromUpdate<State>(
361
payload.call(instance, prevState, nextProps);
362
}
363
}
359
- return payload.call(instance, prevState, nextProps);
364
+ const nextState = payload.call(instance, prevState, nextProps);
365
+ if (__DEV__) {
366
+ exitDisallowedContextReadInDEV();
367
+ }
368
+ return nextState;
369
}
370
// State object
371
return payload;
@@ -372,6 +381,7 @@ function getStateFromUpdate<State>(
381
if (typeof payload === 'function') {
382
// Updater function
383
if (__DEV__) {
384
+ enterDisallowedContextReadInDEV();
385
if (
386
debugRenderPhaseSideEffects ||
387
(debugRenderPhaseSideEffectsForStrictMode &&
@@ -381,6 +391,9 @@ function getStateFromUpdate<State>(
391
}
392
}
393
partialState = payload.call(instance, prevState, nextProps);
394
+ if (__DEV__) {
395
+ exitDisallowedContextReadInDEV();
396
+ }
397
} else {
398
// Partial state object
399
partialState = payload;
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+142
-27
@@ -646,32 +646,20 @@ describe('ReactHooks', () => {
646
);
647
});
648
649
- it('throws when calling hooks inside useMemo', () => {
649
+ it('warns when calling hooks inside useMemo', () => {
650
const {useMemo, useState} = React;
651
function App() {
652
useMemo(() => {
653
useState(0);
654
- return 1;
654
});
655
return null;
656
}
658
-
659
- function Simple() {
660
- const [value] = useState(123);
661
- return value;
662
- }
663
- let root = ReactTestRenderer.create(null);
664
- expect(() => root.update(<App />)).toThrow(
657
+ expect(() => ReactTestRenderer.create(<App />)).toWarnDev(
658
'Hooks can only be called inside the body of a function component',
659
);
667
-
668
- // we want to assure that no hook machinery has broken
669
- // so we render a fresh component with a hook just to be sure
670
- root.update(<Simple />);
671
- expect(root.toJSON()).toEqual('123');
660
});
661
674
- it('throws when reading context inside useMemo', () => {
662
+ it('warns when reading context inside useMemo', () => {
663
const {useMemo, createContext} = React;
664
const ReactCurrentDispatcher =
665
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
@@ -684,12 +672,12 @@ describe('ReactHooks', () => {
672
}, []);
673
}
674
687
- expect(() => ReactTestRenderer.create(<App />)).toThrow(
675
+ expect(() => ReactTestRenderer.create(<App />)).toWarnDev(
676
'Context can only be read while React is rendering',
677
);
678
});
679
692
- it('throws when reading context inside useMemo after reading outside it', () => {
680
+ it('warns when reading context inside useMemo after reading outside it', () => {
681
const {useMemo, createContext} = React;
682
const ReactCurrentDispatcher =
683
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
@@ -706,13 +694,14 @@ describe('ReactHooks', () => {
694
}, []);
695
}
696
709
- expect(() => ReactTestRenderer.create(<App />)).toThrow(
697
+ expect(() => ReactTestRenderer.create(<App />)).toWarnDev(
698
'Context can only be read while React is rendering',
699
);
700
expect(firstRead).toBe('light');
701
expect(secondRead).toBe('light');
702
});
703
704
+ // Throws because there's no runtime cost for being strict here.
705
it('throws when reading context inside useEffect', () => {
706
const {useEffect, createContext} = React;
707
const ReactCurrentDispatcher =
@@ -734,6 +723,7 @@ describe('ReactHooks', () => {
723
);
724
});
725
726
+ // Throws because there's no runtime cost for being strict here.
727
it('throws when reading context inside useLayoutEffect', () => {
728
const {useLayoutEffect, createContext} = React;
729
const ReactCurrentDispatcher =
@@ -754,7 +744,7 @@ describe('ReactHooks', () => {
744
);
745
});
746
757
- it('throws when reading context inside useReducer', () => {
747
+ it('warns when reading context inside useReducer', () => {
748
const {useReducer, createContext} = React;
749
const ReactCurrentDispatcher =
750
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
@@ -772,13 +762,13 @@ describe('ReactHooks', () => {
762
return null;
763
}
764
775
- expect(() => ReactTestRenderer.create(<App />)).toThrow(
765
+ expect(() => ReactTestRenderer.create(<App />)).toWarnDev(
766
'Context can only be read while React is rendering',
767
);
768
});
769
770
// Edge case.
781
- it('throws when reading context inside eager useReducer', () => {
771
+ it('warns when reading context inside eager useReducer', () => {
772
const {useState, createContext} = React;
773
const ThemeContext = createContext('light');
774
@@ -809,20 +799,22 @@ describe('ReactHooks', () => {
799
<Cls />
800
</React.Fragment>,
801
),
812
- ).toThrow('Context can only be read while React is rendering');
802
+ ).toWarnDev('Context can only be read while React is rendering');
803
});
804
815
- it('throws when calling hooks inside useReducer', () => {
805
+ it('warns when calling hooks inside useReducer', () => {
806
const {useReducer, useRef} = React;
807
function App() {
808
const [value, dispatch] = useReducer((state, action) => {
809
useRef(0);
820
- return state;
810
+ return state + 1;
811
}, 0);
822
- dispatch('foo');
812
+ if (value === 0) {
813
+ dispatch('foo');
814
+ }
815
return value;
816
}
825
- expect(() => ReactTestRenderer.create(<App />)).toThrow(
817
+ expect(() => ReactTestRenderer.create(<App />)).toWarnDev(
818
'Hooks can only be called inside the body of a function component',
819
);
820
});
@@ -834,12 +826,107 @@ describe('ReactHooks', () => {
826
useRef(0);
827
return 0;
828
});
829
+ return null;
830
}
838
- expect(() => ReactTestRenderer.create(<App />)).toThrow(
831
+ expect(() => ReactTestRenderer.create(<App />)).toWarnDev(
832
'Hooks can only be called inside the body of a function component',
833
);
834
});
835
836
+ it('resets warning internal state when interrupted by an error', () => {
837
+ const ReactCurrentDispatcher =
838
+ React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
839
+ .ReactCurrentDispatcher;
840
+
841
+ const ThemeContext = React.createContext('light');
842
+ function App() {
843
+ React.useMemo(() => {
844
+ // Trigger warnings
845
+ ReactCurrentDispatcher.current.readContext(ThemeContext);
846
+ React.useRef();
847
+ // Interrupt exit from a Hook
848
+ throw new Error('No.');
849
+ }, []);
850
+ }
851
+
852
+ class Boundary extends React.Component {
853
+ state = {};
854
+ static getDerivedStateFromError(error) {
855
+ return {err: true};
856
+ }
857
+ render() {
858
+ if (this.state.err) {
859
+ return 'Oops';
860
+ }
861
+ return this.props.children;
862
+ }
863
+ }
864
+
865
+ expect(() => {
866
+ ReactTestRenderer.create(
867
+ <Boundary>
868
+ <App />
869
+ </Boundary>,
870
+ );
871
+ }).toWarnDev([
872
+ // We see it twice due to replay
873
+ 'Context can only be read while React is rendering',
874
+ 'Hooks can only be called inside the body of a function component',
875
+ 'Context can only be read while React is rendering',
876
+ 'Hooks can only be called inside the body of a function component',
877
+ ]);
878
+
879
+ function Valid() {
880
+ React.useState();
881
+ React.useMemo(() => {});
882
+ React.useReducer(() => {});
883
+ React.useEffect(() => {});
884
+ React.useLayoutEffect(() => {});
885
+ React.useCallback(() => {});
886
+ React.useRef();
887
+ React.useImperativeHandle(() => {}, () => {});
888
+ if (__DEV__) {
889
+ React.useDebugValue();
890
+ }
891
+ return null;
892
+ }
893
+ // Verify it doesn't think we're still inside a Hook.
894
+ // Should have no warnings.
895
+ ReactTestRenderer.create(<Valid />);
896
+
897
+ // Verify warnings don't get permanently disabled.
898
+ expect(() => {
899
+ ReactTestRenderer.create(
900
+ <Boundary>
901
+ <App />
902
+ </Boundary>,
903
+ );
904
+ }).toWarnDev([
905
+ // We see it twice due to replay
906
+ 'Context can only be read while React is rendering',
907
+ 'Hooks can only be called inside the body of a function component',
908
+ 'Context can only be read while React is rendering',
909
+ 'Hooks can only be called inside the body of a function component',
910
+ ]);
911
+ });
912
+
913
+ it('warns when reading context inside useMemo', () => {
914
+ const {useMemo, createContext} = React;
915
+ const ReactCurrentDispatcher =
916
+ React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
917
+ .ReactCurrentDispatcher;
918
+
919
+ const ThemeContext = createContext('light');
920
+ function App() {
921
+ return useMemo(() => {
922
+ return ReactCurrentDispatcher.current.readContext(ThemeContext);
923
+ }, []);
924
+ }
925
+
926
+ expect(() => ReactTestRenderer.create(<App />)).toWarnDev(
927
+ 'Context can only be read while React is rendering',
928
+ );
929
+ });
930
it('double-invokes components with Hooks in Strict Mode', () => {
931
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = true;
932
@@ -1125,4 +1212,32 @@ describe('ReactHooks', () => {
1212
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
1213
]);
1214
});
1215
+
1216
+ // Regression test for #14674
1217
+ it('does not swallow original error when updating another component in render phase', () => {
1218
+ let {useState} = React;
1219
+
1220
+ let _setState;
1221
+ function A() {
1222
+ const [, setState] = useState(0);
1223
+ _setState = setState;
1224
+ return null;
1225
+ }
1226
+
1227
+ function B() {
1228
+ _setState(() => {
1229
+ throw new Error('Hello');
1230
+ });
1231
+ return null;
1232
+ }
1233
+
1234
+ expect(() =>
1235
+ ReactTestRenderer.create(
1236
+ <React.Fragment>
1237
+ <A />
1238
+ <B />
1239
+ </React.Fragment>,
1240
+ ),
1241
+ ).toThrow('Hello');
1242
+ });
1243
});
packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js
+25
@@ -1346,6 +1346,31 @@ describe('ReactNewContext', () => {
1346
expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1347
expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1348
});
1349
+
1350
+ it('warns when reading context inside render phase class setState updater', () => {
1351
+ const ThemeContext = React.createContext('light');
1352
+
1353
+ class Cls extends React.Component {
1354
+ state = {};
1355
+ render() {
1356
+ this.setState(() => {
1357
+ readContext(ThemeContext);
1358
+ });
1359
+ return null;
1360
+ }
1361
+ }
1362
+
1363
+ ReactNoop.render(<Cls />);
1364
+ expect(ReactNoop.flush).toWarnDev(
1365
+ [
1366
+ 'Context can only be read while React is rendering',
1367
+ 'Cannot update during an existing state transition',
1368
+ ],
1369
+ {
1370
+ withoutStack: 1,
1371
+ },
1372
+ );
1373
+ });
1374
});
1375
1376
describe('useContext', () => {