Skip to content

Commit 3e4949d

Browse files
committed
Make all Hooks-in-Hooks warnings DEV-only
1 parent 63bf091 commit 3e4949d

File tree

4 files changed

+132
-96
lines changed

4 files changed

+132
-96
lines changed

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

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -420,50 +420,47 @@ describe('ReactDOMServerHooks', () => {
420420
},
421421
);
422422

423-
itThrowsWhenRendering(
424-
'a hook inside useMemo',
425-
async render => {
426-
function App() {
427-
useMemo(() => {
428-
useState();
429-
return 0;
430-
});
431-
return null;
432-
}
433-
return render(<App />);
434-
},
435-
'Hooks can only be called inside the body of a function component.',
436-
);
423+
itRenders('with a warning for useState inside useMemo', async render => {
424+
function App() {
425+
useMemo(() => {
426+
useState();
427+
return 0;
428+
});
429+
return 'hi';
430+
}
437431

438-
itThrowsWhenRendering(
439-
'a hook inside useReducer',
440-
async render => {
441-
function App() {
442-
const [value, dispatch] = useReducer((state, action) => {
443-
useRef(0);
444-
return state;
445-
}, 0);
446-
dispatch('foo');
447-
return value;
448-
}
449-
return render(<App />);
450-
},
451-
'Hooks can only be called inside the body of a function component.',
452-
);
432+
const domNode = await render(<App />, 1);
433+
expect(domNode.textContent).toEqual('hi');
434+
});
453435

454-
itThrowsWhenRendering(
455-
'a hook inside useState',
456-
async render => {
457-
function App() {
458-
useState(() => {
459-
useRef(0);
460-
return 0;
461-
});
436+
itRenders('with a warning for useRef inside useReducer', async render => {
437+
function App() {
438+
const [value, dispatch] = useReducer((state, action) => {
439+
useRef(0);
440+
return state + 1;
441+
}, 0);
442+
if (value === 0) {
443+
dispatch();
462444
}
463-
return render(<App />);
464-
},
465-
'Hooks can only be called inside the body of a function component.',
466-
);
445+
return value;
446+
}
447+
448+
const domNode = await render(<App />, 1);
449+
expect(domNode.textContent).toEqual('1');
450+
});
451+
452+
itRenders('with a warning for useRef inside useState', async render => {
453+
function App() {
454+
const [value] = useState(() => {
455+
useRef(0);
456+
return 0;
457+
});
458+
return value;
459+
}
460+
461+
const domNode = await render(<App />, 1);
462+
expect(domNode.textContent).toEqual('0');
463+
});
467464
});
468465

469466
describe('useRef', () => {

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

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ let renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null = null;
4949
let numberOfReRenders: number = 0;
5050
const RE_RENDER_LIMIT = 25;
5151

52-
let shouldWarnAboutReadingContextInDEV = false;
52+
let isInHookUserCodeInDev = false;
5353

5454
// In DEV, this is the name of the currently executing primitive hook
5555
let currentHookNameInDev: ?string;
@@ -59,6 +59,14 @@ function resolveCurrentlyRenderingComponent(): Object {
5959
currentlyRenderingComponent !== null,
6060
'Hooks can only be called inside the body of a function component.',
6161
);
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+
}
6270
return currentlyRenderingComponent;
6371
}
6472

@@ -140,7 +148,7 @@ function createWorkInProgressHook(): Hook {
140148
export function prepareToUseHooks(componentIdentity: Object): void {
141149
currentlyRenderingComponent = componentIdentity;
142150
if (__DEV__) {
143-
shouldWarnAboutReadingContextInDEV = false;
151+
isInHookUserCodeInDev = false;
144152
}
145153

146154
// The following should have already been reset
@@ -179,7 +187,7 @@ export function finishHooks(
179187
renderPhaseUpdates = null;
180188
workInProgressHook = null;
181189
if (__DEV__) {
182-
shouldWarnAboutReadingContextInDEV = false;
190+
isInHookUserCodeInDev = false;
183191
}
184192

185193
// These were reset above
@@ -201,7 +209,7 @@ function readContext<T>(
201209
validateContextBounds(context, threadID);
202210
if (__DEV__) {
203211
warning(
204-
!shouldWarnAboutReadingContextInDEV,
212+
!isInHookUserCodeInDev,
205213
'Context can only be read while React is rendering. ' +
206214
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
207215
'In function components, you can read it directly in the function body, but not ' +
@@ -251,7 +259,7 @@ export function useReducer<S, A>(
251259
currentHookNameInDev = 'useReducer';
252260
}
253261
}
254-
let component = (currentlyRenderingComponent = resolveCurrentlyRenderingComponent());
262+
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
255263
workInProgressHook = createWorkInProgressHook();
256264
if (isReRender) {
257265
// This is a re-render. Apply the new render phase updates to the previous
@@ -270,16 +278,13 @@ export function useReducer<S, A>(
270278
// priority because it will always be the same as the current
271279
// render's.
272280
const action = update.action;
273-
// Temporarily clear to forbid calling Hooks.
274-
currentlyRenderingComponent = null;
275281
if (__DEV__) {
276-
shouldWarnAboutReadingContextInDEV = true;
282+
isInHookUserCodeInDev = true;
277283
}
278284
newState = reducer(newState, action);
279285
if (__DEV__) {
280-
shouldWarnAboutReadingContextInDEV = false;
286+
isInHookUserCodeInDev = false;
281287
}
282-
currentlyRenderingComponent = component;
283288
update = update.next;
284289
} while (update !== null);
285290

@@ -290,7 +295,9 @@ export function useReducer<S, A>(
290295
}
291296
return [workInProgressHook.memoizedState, dispatch];
292297
} else {
293-
currentlyRenderingComponent = null;
298+
if (__DEV__) {
299+
isInHookUserCodeInDev = true;
300+
}
294301
if (reducer === basicStateReducer) {
295302
// Special case for `useState`.
296303
if (typeof initialState === 'function') {
@@ -299,7 +306,9 @@ export function useReducer<S, A>(
299306
} else if (initialAction !== undefined && initialAction !== null) {
300307
initialState = reducer(initialState, initialAction);
301308
}
302-
currentlyRenderingComponent = component;
309+
if (__DEV__) {
310+
isInHookUserCodeInDev = false;
311+
}
303312
workInProgressHook.memoizedState = initialState;
304313
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
305314
last: null,
@@ -315,7 +324,7 @@ export function useReducer<S, A>(
315324
}
316325

317326
function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
318-
let component = (currentlyRenderingComponent = resolveCurrentlyRenderingComponent());
327+
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
319328
workInProgressHook = createWorkInProgressHook();
320329

321330
const nextDeps = deps === undefined ? null : deps;
@@ -332,15 +341,12 @@ function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
332341
}
333342
}
334343

335-
// Temporarily clear to forbid calling Hooks.
336-
currentlyRenderingComponent = null;
337344
if (__DEV__) {
338-
shouldWarnAboutReadingContextInDEV = true;
345+
isInHookUserCodeInDev = true;
339346
}
340347
const nextValue = nextCreate();
341-
currentlyRenderingComponent = component;
342348
if (__DEV__) {
343-
shouldWarnAboutReadingContextInDEV = false;
349+
isInHookUserCodeInDev = false;
344350
}
345351
workInProgressHook.memoizedState = [nextValue, nextDeps];
346352
return nextValue;

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type HookType =
7373
// the first instance of a hook mismatch in a component,
7474
// represented by a portion of it's stacktrace
7575
let currentHookMismatchInDev = null;
76+
let isInHookUserCodeInDev = false;
7677

7778
let didWarnAboutMismatchedHooksForComponent;
7879
if (__DEV__) {
@@ -154,6 +155,17 @@ function resolveCurrentlyRenderingFiber(): Fiber {
154155
currentlyRenderingFiber !== null,
155156
'Hooks can only be called inside the body of a function component.',
156157
);
158+
if (__DEV__) {
159+
// Check if we're inside Hooks like useMemo(). DEV-only for perf.
160+
// TODO: we can make a better warning message with currentHookNameInDev
161+
// if we also make sure it's consistently assigned in the right order.
162+
warning(
163+
!isInHookUserCodeInDev,
164+
'Hooks can only be called inside the body of a function component. ' +
165+
'Do not call Hooks inside other Hooks. For more information, see ' +
166+
'https://fb.me/rules-of-hooks',
167+
);
168+
}
157169
return currentlyRenderingFiber;
158170
}
159171

@@ -580,7 +592,7 @@ export function useReducer<S, A>(
580592
currentHookNameInDev = 'useReducer';
581593
}
582594
}
583-
let fiber = (currentlyRenderingFiber = resolveCurrentlyRenderingFiber());
595+
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
584596
workInProgressHook = createWorkInProgressHook();
585597
if (__DEV__) {
586598
currentHookNameInDev = null;
@@ -604,15 +616,14 @@ export function useReducer<S, A>(
604616
// priority because it will always be the same as the current
605617
// render's.
606618
const action = update.action;
607-
// Temporarily clear to forbid calling Hooks in a reducer.
608-
currentlyRenderingFiber = null;
609619
if (__DEV__) {
610620
stashContextDependenciesInDEV();
621+
isInHookUserCodeInDev = true;
611622
}
612623
newState = reducer(newState, action);
613-
currentlyRenderingFiber = fiber;
614624
if (__DEV__) {
615625
unstashContextDependenciesInDEV();
626+
isInHookUserCodeInDev = false;
616627
}
617628
update = update.next;
618629
} while (update !== null);
@@ -682,15 +693,14 @@ export function useReducer<S, A>(
682693
newState = ((update.eagerState: any): S);
683694
} else {
684695
const action = update.action;
685-
// Temporarily clear to forbid calling Hooks in a reducer.
686-
currentlyRenderingFiber = null;
687696
if (__DEV__) {
688697
stashContextDependenciesInDEV();
698+
isInHookUserCodeInDev = true;
689699
}
690700
newState = reducer(newState, action);
691-
currentlyRenderingFiber = fiber;
692701
if (__DEV__) {
693702
unstashContextDependenciesInDEV();
703+
isInHookUserCodeInDev = false;
694704
}
695705
}
696706
}
@@ -720,10 +730,9 @@ export function useReducer<S, A>(
720730
const dispatch: Dispatch<A> = (queue.dispatch: any);
721731
return [workInProgressHook.memoizedState, dispatch];
722732
}
723-
// Temporarily clear to forbid calling Hooks in a reducer.
724-
currentlyRenderingFiber = null;
725733
if (__DEV__) {
726734
stashContextDependenciesInDEV();
735+
isInHookUserCodeInDev = true;
727736
}
728737
// There's no existing queue, so this is the initial render.
729738
if (reducer === basicStateReducer) {
@@ -734,9 +743,9 @@ export function useReducer<S, A>(
734743
} else if (initialAction !== undefined && initialAction !== null) {
735744
initialState = reducer(initialState, initialAction);
736745
}
737-
currentlyRenderingFiber = fiber;
738746
if (__DEV__) {
739747
unstashContextDependenciesInDEV();
748+
isInHookUserCodeInDev = false;
740749
}
741750
workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;
742751
queue = workInProgressHook.queue = {
@@ -960,7 +969,7 @@ export function useMemo<T>(
960969
if (__DEV__) {
961970
currentHookNameInDev = 'useMemo';
962971
}
963-
let fiber = (currentlyRenderingFiber = resolveCurrentlyRenderingFiber());
972+
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
964973
workInProgressHook = createWorkInProgressHook();
965974

966975
const nextDeps = deps === undefined ? null : deps;
@@ -979,15 +988,14 @@ export function useMemo<T>(
979988
}
980989
}
981990

982-
// Temporarily clear to forbid calling Hooks.
983-
currentlyRenderingFiber = null;
984991
if (__DEV__) {
985992
stashContextDependenciesInDEV();
993+
isInHookUserCodeInDev = true;
986994
}
987995
const nextValue = nextCreate();
988-
currentlyRenderingFiber = fiber;
989996
if (__DEV__) {
990997
unstashContextDependenciesInDEV();
998+
isInHookUserCodeInDev = false;
991999
}
9921000
workInProgressHook.memoizedState = [nextValue, nextDeps];
9931001
if (__DEV__) {
@@ -1086,16 +1094,14 @@ function dispatchAction<S, A>(
10861094
if (eagerReducer !== null) {
10871095
try {
10881096
const currentState: S = (queue.eagerState: any);
1089-
// Temporarily clear to forbid calling Hooks in a reducer.
1090-
let maybeFiber = currentlyRenderingFiber; // Note: likely null now unlike `fiber`
1091-
currentlyRenderingFiber = null;
10921097
if (__DEV__) {
10931098
stashContextDependenciesInDEV();
1099+
isInHookUserCodeInDev = true;
10941100
}
10951101
const eagerState = eagerReducer(currentState, action);
1096-
currentlyRenderingFiber = maybeFiber;
10971102
if (__DEV__) {
10981103
unstashContextDependenciesInDEV();
1104+
isInHookUserCodeInDev = false;
10991105
}
11001106
// Stash the eagerly computed state, and the reducer used to compute
11011107
// it, on the update object. If the reducer hasn't changed by the
@@ -1112,6 +1118,10 @@ function dispatchAction<S, A>(
11121118
}
11131119
} catch (error) {
11141120
// Suppress the error. It will throw again in the render phase.
1121+
} finally {
1122+
if (__DEV__) {
1123+
isInHookUserCodeInDev = false;
1124+
}
11151125
}
11161126
}
11171127
}

0 commit comments

Comments
 (0)