Skip to content

Commit 8bcc88f

Browse files
authored
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
1 parent 6cb2677 commit 8bcc88f

7 files changed

+388
-129
lines changed

packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js

+70-41
Original file line numberDiff line numberDiff line change
@@ -419,50 +419,47 @@ describe('ReactDOMServerHooks', () => {
419419
},
420420
);
421421

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+
}
436430

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+
});
452434

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();
461443
}
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+
});
466463
});
467464

468465
describe('useRef', () => {
@@ -716,4 +713,36 @@ describe('ReactDOMServerHooks', () => {
716713
expect(domNode.textContent).toEqual('undefined');
717714
});
718715
});
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+
});
719748
});

packages/react-dom/src/server/ReactPartialRendererHooks.js

+45-10
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ let renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null = null;
4949
let numberOfReRenders: number = 0;
5050
const RE_RENDER_LIMIT = 25;
5151

52+
let isInHookUserCodeInDev = false;
53+
5254
// In DEV, this is the name of the currently executing primitive hook
5355
let currentHookNameInDev: ?string;
5456

@@ -57,6 +59,14 @@ function resolveCurrentlyRenderingComponent(): Object {
5759
currentlyRenderingComponent !== null,
5860
'Hooks can only be called inside the body of a function component.',
5961
);
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+
}
6070
return currentlyRenderingComponent;
6171
}
6272

@@ -137,6 +147,9 @@ function createWorkInProgressHook(): Hook {
137147

138148
export function prepareToUseHooks(componentIdentity: Object): void {
139149
currentlyRenderingComponent = componentIdentity;
150+
if (__DEV__) {
151+
isInHookUserCodeInDev = false;
152+
}
140153

141154
// The following should have already been reset
142155
// didScheduleRenderPhaseUpdate = false;
@@ -173,6 +186,9 @@ export function finishHooks(
173186
numberOfReRenders = 0;
174187
renderPhaseUpdates = null;
175188
workInProgressHook = null;
189+
if (__DEV__) {
190+
isInHookUserCodeInDev = false;
191+
}
176192

177193
// These were reset above
178194
// currentlyRenderingComponent = null;
@@ -191,6 +207,15 @@ function readContext<T>(
191207
): T {
192208
let threadID = currentThreadID;
193209
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+
}
194219
return context[threadID];
195220
}
196221

@@ -234,7 +259,7 @@ export function useReducer<S, A>(
234259
currentHookNameInDev = 'useReducer';
235260
}
236261
}
237-
let component = (currentlyRenderingComponent = resolveCurrentlyRenderingComponent());
262+
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
238263
workInProgressHook = createWorkInProgressHook();
239264
if (isReRender) {
240265
// This is a re-render. Apply the new render phase updates to the previous
@@ -253,10 +278,13 @@ export function useReducer<S, A>(
253278
// priority because it will always be the same as the current
254279
// render's.
255280
const action = update.action;
256-
// Temporarily clear to forbid calling Hooks.
257-
currentlyRenderingComponent = null;
281+
if (__DEV__) {
282+
isInHookUserCodeInDev = true;
283+
}
258284
newState = reducer(newState, action);
259-
currentlyRenderingComponent = component;
285+
if (__DEV__) {
286+
isInHookUserCodeInDev = false;
287+
}
260288
update = update.next;
261289
} while (update !== null);
262290

@@ -267,7 +295,9 @@ export function useReducer<S, A>(
267295
}
268296
return [workInProgressHook.memoizedState, dispatch];
269297
} else {
270-
currentlyRenderingComponent = null;
298+
if (__DEV__) {
299+
isInHookUserCodeInDev = true;
300+
}
271301
if (reducer === basicStateReducer) {
272302
// Special case for `useState`.
273303
if (typeof initialState === 'function') {
@@ -276,7 +306,9 @@ export function useReducer<S, A>(
276306
} else if (initialAction !== undefined && initialAction !== null) {
277307
initialState = reducer(initialState, initialAction);
278308
}
279-
currentlyRenderingComponent = component;
309+
if (__DEV__) {
310+
isInHookUserCodeInDev = false;
311+
}
280312
workInProgressHook.memoizedState = initialState;
281313
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
282314
last: null,
@@ -292,7 +324,7 @@ export function useReducer<S, A>(
292324
}
293325

294326
function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
295-
let component = (currentlyRenderingComponent = resolveCurrentlyRenderingComponent());
327+
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
296328
workInProgressHook = createWorkInProgressHook();
297329

298330
const nextDeps = deps === undefined ? null : deps;
@@ -309,10 +341,13 @@ function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
309341
}
310342
}
311343

312-
// Temporarily clear to forbid calling Hooks.
313-
currentlyRenderingComponent = null;
344+
if (__DEV__) {
345+
isInHookUserCodeInDev = true;
346+
}
314347
const nextValue = nextCreate();
315-
currentlyRenderingComponent = component;
348+
if (__DEV__) {
349+
isInHookUserCodeInDev = false;
350+
}
316351
workInProgressHook.memoizedState = [nextValue, nextDeps];
317352
return nextValue;
318353
}

0 commit comments

Comments
 (0)