Skip to content

Commit 7085e70

Browse files
committed
Make all Hooks-in-Hooks warnings DEV-only
1 parent b8b828c commit 7085e70

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

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
@@ -72,6 +72,7 @@ type HookType =
7272
// the first instance of a hook mismatch in a component,
7373
// represented by a portion of it's stacktrace
7474
let currentHookMismatchInDev = null;
75+
let isInHookUserCodeInDev = false;
7576

7677
let didWarnAboutMismatchedHooksForComponent;
7778
if (__DEV__) {
@@ -153,6 +154,17 @@ function resolveCurrentlyRenderingFiber(): Fiber {
153154
currentlyRenderingFiber !== null,
154155
'Hooks can only be called inside the body of a function component.',
155156
);
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+
}
156168
return currentlyRenderingFiber;
157169
}
158170

@@ -573,7 +585,7 @@ export function useReducer<S, A>(
573585
currentHookNameInDev = 'useReducer';
574586
}
575587
}
576-
let fiber = (currentlyRenderingFiber = resolveCurrentlyRenderingFiber());
588+
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
577589
workInProgressHook = createWorkInProgressHook();
578590
if (__DEV__) {
579591
currentHookNameInDev = null;
@@ -597,15 +609,14 @@ export function useReducer<S, A>(
597609
// priority because it will always be the same as the current
598610
// render's.
599611
const action = update.action;
600-
// Temporarily clear to forbid calling Hooks in a reducer.
601-
currentlyRenderingFiber = null;
602612
if (__DEV__) {
603613
stashContextDependenciesInDEV();
614+
isInHookUserCodeInDev = true;
604615
}
605616
newState = reducer(newState, action);
606-
currentlyRenderingFiber = fiber;
607617
if (__DEV__) {
608618
unstashContextDependenciesInDEV();
619+
isInHookUserCodeInDev = false;
609620
}
610621
update = update.next;
611622
} while (update !== null);
@@ -675,15 +686,14 @@ export function useReducer<S, A>(
675686
newState = ((update.eagerState: any): S);
676687
} else {
677688
const action = update.action;
678-
// Temporarily clear to forbid calling Hooks in a reducer.
679-
currentlyRenderingFiber = null;
680689
if (__DEV__) {
681690
stashContextDependenciesInDEV();
691+
isInHookUserCodeInDev = true;
682692
}
683693
newState = reducer(newState, action);
684-
currentlyRenderingFiber = fiber;
685694
if (__DEV__) {
686695
unstashContextDependenciesInDEV();
696+
isInHookUserCodeInDev = false;
687697
}
688698
}
689699
}
@@ -713,10 +723,9 @@ export function useReducer<S, A>(
713723
const dispatch: Dispatch<A> = (queue.dispatch: any);
714724
return [workInProgressHook.memoizedState, dispatch];
715725
}
716-
// Temporarily clear to forbid calling Hooks in a reducer.
717-
currentlyRenderingFiber = null;
718726
if (__DEV__) {
719727
stashContextDependenciesInDEV();
728+
isInHookUserCodeInDev = true;
720729
}
721730
// There's no existing queue, so this is the initial render.
722731
if (reducer === basicStateReducer) {
@@ -727,9 +736,9 @@ export function useReducer<S, A>(
727736
} else if (initialAction !== undefined && initialAction !== null) {
728737
initialState = reducer(initialState, initialAction);
729738
}
730-
currentlyRenderingFiber = fiber;
731739
if (__DEV__) {
732740
unstashContextDependenciesInDEV();
741+
isInHookUserCodeInDev = false;
733742
}
734743
workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;
735744
queue = workInProgressHook.queue = {
@@ -953,7 +962,7 @@ export function useMemo<T>(
953962
if (__DEV__) {
954963
currentHookNameInDev = 'useMemo';
955964
}
956-
let fiber = (currentlyRenderingFiber = resolveCurrentlyRenderingFiber());
965+
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
957966
workInProgressHook = createWorkInProgressHook();
958967

959968
const nextDeps = deps === undefined ? null : deps;
@@ -972,15 +981,14 @@ export function useMemo<T>(
972981
}
973982
}
974983

975-
// Temporarily clear to forbid calling Hooks.
976-
currentlyRenderingFiber = null;
977984
if (__DEV__) {
978985
stashContextDependenciesInDEV();
986+
isInHookUserCodeInDev = true;
979987
}
980988
const nextValue = nextCreate();
981-
currentlyRenderingFiber = fiber;
982989
if (__DEV__) {
983990
unstashContextDependenciesInDEV();
991+
isInHookUserCodeInDev = false;
984992
}
985993
workInProgressHook.memoizedState = [nextValue, nextDeps];
986994
if (__DEV__) {
@@ -1079,16 +1087,14 @@ function dispatchAction<S, A>(
10791087
if (eagerReducer !== null) {
10801088
try {
10811089
const currentState: S = (queue.eagerState: any);
1082-
// Temporarily clear to forbid calling Hooks in a reducer.
1083-
let maybeFiber = currentlyRenderingFiber; // Note: likely null now unlike `fiber`
1084-
currentlyRenderingFiber = null;
10851090
if (__DEV__) {
10861091
stashContextDependenciesInDEV();
1092+
isInHookUserCodeInDev = true;
10871093
}
10881094
const eagerState = eagerReducer(currentState, action);
1089-
currentlyRenderingFiber = maybeFiber;
10901095
if (__DEV__) {
10911096
unstashContextDependenciesInDEV();
1097+
isInHookUserCodeInDev = false;
10921098
}
10931099
// Stash the eagerly computed state, and the reducer used to compute
10941100
// it, on the update object. If the reducer hasn't changed by the
@@ -1105,6 +1111,10 @@ function dispatchAction<S, A>(
11051111
}
11061112
} catch (error) {
11071113
// Suppress the error. It will throw again in the render phase.
1114+
} finally {
1115+
if (__DEV__) {
1116+
isInHookUserCodeInDev = false;
1117+
}
11081118
}
11091119
}
11101120
}

0 commit comments

Comments
 (0)