@samitouri / QOS-React / commits / 8bcc88f2e7

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', () => {