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() {