@samitouri / QOS-React / commits / a129259ad6

Disallow reading context during useMemo etc (#14653)

* Revert "Revert "Double-render function components with Hooks in DEV in StrictMode" (#14652)" This reverts commit 3fbebb2a0b3b806f428c4154700af8e75e561895. * Revert "Revert "Disallow reading context during useMemo etc" (#14651)" This reverts commit 5fce6488ce1dc97e31515a47ff409d32ab722d65. * Add extra passing test for an edge case Mentioned by @acdlite to watch out for * More convoluted test * Don't rely on expirationTime Addresses @acdlite's concerns * Edge case: forbid readContext() during eager reducer

Dan Abramov committed Jan 23, 2019 at 15:51 UTC a129259ad6a61790fe1912a87cb45018d942f90c
4 files changed +186 -9
packages/react-reconciler/src/ReactFiberClassComponent.js
+1 -8
@@ -20,7 +20,6 @@ import {
20 import ReactStrictModeWarnings from './ReactStrictModeWarnings';
21 import {isMounted} from 'react-reconciler/reflection';
22 import {get as getInstance, set as setInstance} from 'shared/ReactInstanceMap';
23 -import ReactSharedInternals from 'shared/ReactSharedInternals';
23 import shallowEqual from 'shared/shallowEqual';
24 import getComponentName from 'shared/getComponentName';
25 import invariant from 'shared/invariant';
@@ -48,6 +47,7 @@ import {
47 hasContextChanged,
48 emptyContextObject,
49 } from './ReactFiberContext';
50 +import {readContext} from './ReactFiberNewContext';
51 import {
52 requestCurrentTime,
53 computeExpirationForFiber,
@@ -55,13 +55,6 @@ import {
55 flushPassiveEffects,
56 } from './ReactFiberScheduler';
57
58 -const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
59 -
60 -function readContext(contextType: any): any {
61 - const dispatcher = ReactCurrentDispatcher.current;
62 - return dispatcher.readContext(contextType);
63 -}
64 -
58 const fakeInternalInstance = {};
59 const isArray = Array.isArray;
60
packages/react-reconciler/src/ReactFiberHooks.js
+19 -1
@@ -14,7 +14,11 @@ import type {HookEffectTag} from './ReactHookEffectTags';
14
15 import {NoWork} from './ReactFiberExpirationTime';
16 import {enableHooks} from 'shared/ReactFeatureFlags';
17 -import {readContext} from './ReactFiberNewContext';
17 +import {
18 + readContext,
19 + stashContextDependencies,
20 + unstashContextDependencies,
21 +} from './ReactFiberNewContext';
22 import {
23 Update as UpdateEffect,
24 Passive as PassiveEffect,
@@ -600,8 +604,10 @@ export function useReducer<S, A>(
604 const action = update.action;
605 // Temporarily clear to forbid calling Hooks in a reducer.
606 currentlyRenderingFiber = null;
607 + stashContextDependencies();
608 newState = reducer(newState, action);
609 currentlyRenderingFiber = fiber;
610 + unstashContextDependencies();
611 update = update.next;
612 } while (update !== null);
613
@@ -672,8 +678,10 @@ export function useReducer<S, A>(
678 const action = update.action;
679 // Temporarily clear to forbid calling Hooks in a reducer.
680 currentlyRenderingFiber = null;
681 + stashContextDependencies();
682 newState = reducer(newState, action);
683 currentlyRenderingFiber = fiber;
684 + unstashContextDependencies();
685 }
686 }
687 prevUpdate = update;
@@ -704,6 +712,7 @@ export function useReducer<S, A>(
712 }
713 // Temporarily clear to forbid calling Hooks in a reducer.
714 currentlyRenderingFiber = null;
715 + stashContextDependencies();
716 // There's no existing queue, so this is the initial render.
717 if (reducer === basicStateReducer) {
718 // Special case for `useState`.
@@ -714,6 +723,7 @@ export function useReducer<S, A>(
723 initialState = reducer(initialState, initialAction);
724 }
725 currentlyRenderingFiber = fiber;
726 + unstashContextDependencies();
727 workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;
728 queue = workInProgressHook.queue = {
729 last: null,
@@ -947,8 +957,10 @@ export function useMemo<T>(
957
958 // Temporarily clear to forbid calling Hooks.
959 currentlyRenderingFiber = null;
960 + stashContextDependencies();
961 const nextValue = nextCreate();
962 currentlyRenderingFiber = fiber;
963 + unstashContextDependencies();
964 workInProgressHook.memoizedState = [nextValue, nextDeps];
965 currentHookNameInDev = null;
966 return nextValue;
@@ -1044,7 +1056,13 @@ function dispatchAction<S, A>(
1056 if (eagerReducer !== null) {
1057 try {
1058 const currentState: S = (queue.eagerState: any);
1059 + // Temporarily clear to forbid calling Hooks in a reducer.
1060 + let maybeFiber = currentlyRenderingFiber; // Note: likely null now unlike `fiber`
1061 + currentlyRenderingFiber = null;
1062 + stashContextDependencies();
1063 const eagerState = eagerReducer(currentState, action);
1064 + currentlyRenderingFiber = maybeFiber;
1065 + unstashContextDependencies();
1066 // Stash the eagerly computed state, and the reducer used to compute
1067 // it, on the update object. If the reducer hasn't changed by the
1068 // time we enter the render phase, then the eager state can be used
packages/react-reconciler/src/ReactFiberNewContext.js
+25
@@ -52,12 +52,37 @@ 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;
59 +
60 export function resetContextDependences(): void {
61 // This is called right before React yields execution, to ensure `readContext`
62 // cannot be called outside the render phase.
63 currentlyRenderingFiber = null;
64 lastContextDependency = null;
65 lastContextWithAllBitsObserved = null;
66 +
67 + stashedCurrentlyRenderingFiber = null;
68 + stashedLastContextDependency = null;
69 + stashedLastContextWithAllBitsObserved = null;
70 +}
71 +
72 +export function stashContextDependencies(): void {
73 + stashedCurrentlyRenderingFiber = currentlyRenderingFiber;
74 + stashedLastContextDependency = lastContextDependency;
75 + stashedLastContextWithAllBitsObserved = lastContextWithAllBitsObserved;
76 +
77 + currentlyRenderingFiber = null;
78 + lastContextDependency = null;
79 + lastContextWithAllBitsObserved = null;
80 +}
81 +
82 +export function unstashContextDependencies(): void {
83 + currentlyRenderingFiber = stashedCurrentlyRenderingFiber;
84 + lastContextDependency = stashedLastContextDependency;
85 + lastContextWithAllBitsObserved = stashedLastContextWithAllBitsObserved;
86 }
87
88 export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+141
@@ -672,6 +672,147 @@ describe('ReactHooks', () => {
672 expect(root.toJSON()).toEqual('123');
673 });
674
675 + it('throws when reading context inside useMemo', () => {
676 + const {useMemo, createContext} = React;
677 + const ReactCurrentDispatcher =
678 + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
679 + .ReactCurrentDispatcher;
680 +
681 + const ThemeContext = createContext('light');
682 + function App() {
683 + return useMemo(() => {
684 + return ReactCurrentDispatcher.current.readContext(ThemeContext);
685 + }, []);
686 + }
687 +
688 + expect(() => ReactTestRenderer.create(<App />)).toThrow(
689 + 'Context can only be read while React is rendering',
690 + );
691 + });
692 +
693 + it('throws when reading context inside useMemo after reading outside it', () => {
694 + const {useMemo, createContext} = React;
695 + const ReactCurrentDispatcher =
696 + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
697 + .ReactCurrentDispatcher;
698 +
699 + const ThemeContext = createContext('light');
700 + let firstRead, secondRead;
701 + function App() {
702 + firstRead = ReactCurrentDispatcher.current.readContext(ThemeContext);
703 + useMemo(() => {});
704 + secondRead = ReactCurrentDispatcher.current.readContext(ThemeContext);
705 + return useMemo(() => {
706 + return ReactCurrentDispatcher.current.readContext(ThemeContext);
707 + }, []);
708 + }
709 +
710 + expect(() => ReactTestRenderer.create(<App />)).toThrow(
711 + 'Context can only be read while React is rendering',
712 + );
713 + expect(firstRead).toBe('light');
714 + expect(secondRead).toBe('light');
715 + });
716 +
717 + it('throws when reading context inside useEffect', () => {
718 + const {useEffect, createContext} = React;
719 + const ReactCurrentDispatcher =
720 + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
721 + .ReactCurrentDispatcher;
722 +
723 + const ThemeContext = createContext('light');
724 + function App() {
725 + useEffect(() => {
726 + ReactCurrentDispatcher.current.readContext(ThemeContext);
727 + });
728 + return null;
729 + }
730 +
731 + const root = ReactTestRenderer.create(<App />);
732 + expect(() => root.update(<App />)).toThrow(
733 + // The exact message doesn't matter, just make sure we don't allow this
734 + "Cannot read property 'readContext' of null",
735 + );
736 + });
737 +
738 + it('throws when reading context inside useLayoutEffect', () => {
739 + const {useLayoutEffect, createContext} = React;
740 + const ReactCurrentDispatcher =
741 + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
742 + .ReactCurrentDispatcher;
743 +
744 + const ThemeContext = createContext('light');
745 + function App() {
746 + useLayoutEffect(() => {
747 + ReactCurrentDispatcher.current.readContext(ThemeContext);
748 + });
749 + return null;
750 + }
751 +
752 + expect(() => ReactTestRenderer.create(<App />)).toThrow(
753 + // The exact message doesn't matter, just make sure we don't allow this
754 + "Cannot read property 'readContext' of null",
755 + );
756 + });
757 +
758 + it('throws when reading context inside useReducer', () => {
759 + const {useReducer, createContext} = React;
760 + const ReactCurrentDispatcher =
761 + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
762 + .ReactCurrentDispatcher;
763 +
764 + const ThemeContext = createContext('light');
765 + function App() {
766 + useReducer(
767 + () => {
768 + ReactCurrentDispatcher.current.readContext(ThemeContext);
769 + },
770 + null,
771 + {},
772 + );
773 + return null;
774 + }
775 +
776 + expect(() => ReactTestRenderer.create(<App />)).toThrow(
777 + 'Context can only be read while React is rendering',
778 + );
779 + });
780 +
781 + // Edge case.
782 + it('throws when reading context inside eager useReducer', () => {
783 + const {useState, createContext} = React;
784 + const ThemeContext = createContext('light');
785 +
786 + const ReactCurrentDispatcher =
787 + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
788 + .ReactCurrentDispatcher;
789 +
790 + let _setState;
791 + function Fn() {
792 + const [, setState] = useState(0);
793 + _setState = setState;
794 + return null;
795 + }
796 +
797 + class Cls extends React.Component {
798 + render() {
799 + _setState(() => {
800 + ReactCurrentDispatcher.current.readContext(ThemeContext);
801 + });
802 + return null;
803 + }
804 + }
805 +
806 + expect(() =>
807 + ReactTestRenderer.create(
808 + <React.Fragment>
809 + <Fn />
810 + <Cls />
811 + </React.Fragment>,
812 + ),
813 + ).toThrow('Context can only be read while React is rendering');
814 + });
815 +
816 it('throws when calling hooks inside useReducer', () => {
817 const {useReducer, useRef} = React;
818 function App() {