Skip to content

Commit ecd919a

Browse files
author
Sunil Pai
authored
RFC: warn when returning different hooks on subsequent renders (#14585)
* warn when returning different hooks on next render like it says. adds a field to Hook to track effect 'type', and compares when cloning subsequently. * lint * review changes - numbered enum for hook types - s/hookType/_debugType - better dce * cleaner detection location * redundant comments * different EffectHook / LayoutEffectHook * prettier * top level currentHookType * nulling currentHookType need to verify dce still works * small enhancements * hook order checks for useContext/useImperative * prettier * stray whitespace * move some bits around * better errors * pass tests * lint, flow * show a before - after diff * an error stack in the warning * lose currentHookMatches, fix a test * tidy * clear the mismatch only in dev * pass flow * side by side diff * tweak warning * pass flow * dedupe warnings per fiber, nits * better format * nit * fix bad merge, pass flow * lint * missing hooktype enum * merge currentHookType/currentHookNameInDev, fix nits * lint * final nits
1 parent 3fbebb2 commit ecd919a

File tree

2 files changed

+253
-18
lines changed

2 files changed

+253
-18
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

+187-18
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,27 @@ type UpdateQueue<S, A> = {
5454
eagerState: S | null,
5555
};
5656

57+
type HookType =
58+
| 'useState'
59+
| 'useReducer'
60+
| 'useContext'
61+
| 'useRef'
62+
| 'useEffect'
63+
| 'useLayoutEffect'
64+
| 'useCallback'
65+
| 'useMemo'
66+
| 'useImperativeHandle'
67+
| 'useDebugValue';
68+
69+
// the first instance of a hook mismatch in a component,
70+
// represented by a portion of it's stacktrace
71+
let currentHookMismatch = null;
72+
73+
let didWarnAboutMismatchedHooksForComponent;
74+
if (__DEV__) {
75+
didWarnAboutMismatchedHooksForComponent = new Set();
76+
}
77+
5778
export type Hook = {
5879
memoizedState: any,
5980

@@ -64,6 +85,10 @@ export type Hook = {
6485
next: Hook | null,
6586
};
6687

88+
type HookDev = Hook & {
89+
_debugType: HookType,
90+
};
91+
6792
type Effect = {
6893
tag: HookEffectTag,
6994
create: () => mixed,
@@ -118,7 +143,7 @@ let numberOfReRenders: number = -1;
118143
const RE_RENDER_LIMIT = 25;
119144

120145
// In DEV, this is the name of the currently executing primitive hook
121-
let currentHookNameInDev: ?string;
146+
let currentHookNameInDev: ?HookType = null;
122147

123148
function resolveCurrentlyRenderingFiber(): Fiber {
124149
invariant(
@@ -170,6 +195,95 @@ function areHookInputsEqual(
170195
return true;
171196
}
172197

198+
// till we have String::padEnd, a small function to
199+
// right-pad strings with spaces till a minimum length
200+
function padEndSpaces(string: string, length: number) {
201+
if (__DEV__) {
202+
if (string.length >= length) {
203+
return string;
204+
} else {
205+
return string + ' ' + new Array(length - string.length).join(' ');
206+
}
207+
}
208+
}
209+
210+
function flushHookMismatchWarnings() {
211+
// we'll show the diff of the low level hooks,
212+
// and a stack trace so the dev can locate where
213+
// the first mismatch is coming from
214+
if (__DEV__) {
215+
if (currentHookMismatch !== null) {
216+
let componentName = getComponentName(
217+
((currentlyRenderingFiber: any): Fiber).type,
218+
);
219+
if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) {
220+
didWarnAboutMismatchedHooksForComponent.add(componentName);
221+
const hookStackDiff = [];
222+
let current = firstCurrentHook;
223+
const previousOrder = [];
224+
while (current !== null) {
225+
previousOrder.push(((current: any): HookDev)._debugType);
226+
current = current.next;
227+
}
228+
let workInProgress = firstWorkInProgressHook;
229+
const nextOrder = [];
230+
while (workInProgress !== null) {
231+
nextOrder.push(((workInProgress: any): HookDev)._debugType);
232+
workInProgress = workInProgress.next;
233+
}
234+
// some bookkeeping for formatting the output table
235+
const columnLength = Math.max.apply(
236+
null,
237+
previousOrder
238+
.map(hook => hook.length)
239+
.concat(' Previous render'.length),
240+
);
241+
242+
let hookStackHeader =
243+
((padEndSpaces(' Previous render', columnLength): any): string) +
244+
' Next render\n';
245+
const hookStackWidth = hookStackHeader.length;
246+
hookStackHeader += ' ' + new Array(hookStackWidth - 2).join('-');
247+
const hookStackFooter = ' ' + new Array(hookStackWidth - 2).join('^');
248+
249+
const hookStackLength = Math.max(
250+
previousOrder.length,
251+
nextOrder.length,
252+
);
253+
for (let i = 0; i < hookStackLength; i++) {
254+
hookStackDiff.push(
255+
((padEndSpaces(i + 1 + '. ', 3): any): string) +
256+
((padEndSpaces(previousOrder[i], columnLength): any): string) +
257+
' ' +
258+
nextOrder[i],
259+
);
260+
if (previousOrder[i] !== nextOrder[i]) {
261+
break;
262+
}
263+
}
264+
warning(
265+
false,
266+
'React has detected a change in the order of Hooks called by %s. ' +
267+
'This will lead to bugs and errors if not fixed. ' +
268+
'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
269+
'%s\n' +
270+
'%s\n' +
271+
'%s\n' +
272+
'The first Hook type mismatch occured at:\n' +
273+
'%s\n\n' +
274+
'This error occurred in the following component:',
275+
componentName,
276+
hookStackHeader,
277+
hookStackDiff.join('\n'),
278+
hookStackFooter,
279+
currentHookMismatch,
280+
);
281+
}
282+
currentHookMismatch = null;
283+
}
284+
}
285+
}
286+
173287
export function renderWithHooks(
174288
current: Fiber | null,
175289
workInProgress: Fiber,
@@ -221,6 +335,7 @@ export function renderWithHooks(
221335
getComponentName(Component),
222336
);
223337
}
338+
flushHookMismatchWarnings();
224339
}
225340
} while (didScheduleRenderPhaseUpdate);
226341

@@ -248,7 +363,7 @@ export function renderWithHooks(
248363
componentUpdateQueue = null;
249364

250365
if (__DEV__) {
251-
currentHookNameInDev = undefined;
366+
currentHookNameInDev = null;
252367
}
253368

254369
// These were reset above
@@ -281,6 +396,9 @@ export function resetHooks(): void {
281396
if (!enableHooks) {
282397
return;
283398
}
399+
if (__DEV__) {
400+
flushHookMismatchWarnings();
401+
}
284402

285403
// This is used to reset the state of this module when a component throws.
286404
// It's also called inside mountIndeterminateComponent if we determine the
@@ -297,7 +415,7 @@ export function resetHooks(): void {
297415
componentUpdateQueue = null;
298416

299417
if (__DEV__) {
300-
currentHookNameInDev = undefined;
418+
currentHookNameInDev = null;
301419
}
302420

303421
didScheduleRenderPhaseUpdate = false;
@@ -306,27 +424,63 @@ export function resetHooks(): void {
306424
}
307425

308426
function createHook(): Hook {
309-
return {
310-
memoizedState: null,
427+
let hook: Hook = __DEV__
428+
? {
429+
_debugType: ((currentHookNameInDev: any): HookType),
430+
memoizedState: null,
311431

312-
baseState: null,
313-
queue: null,
314-
baseUpdate: null,
432+
baseState: null,
433+
queue: null,
434+
baseUpdate: null,
315435

316-
next: null,
317-
};
436+
next: null,
437+
}
438+
: {
439+
memoizedState: null,
440+
441+
baseState: null,
442+
queue: null,
443+
baseUpdate: null,
444+
445+
next: null,
446+
};
447+
448+
return hook;
318449
}
319450

320451
function cloneHook(hook: Hook): Hook {
321-
return {
322-
memoizedState: hook.memoizedState,
452+
let nextHook: Hook = __DEV__
453+
? {
454+
_debugType: ((currentHookNameInDev: any): HookType),
455+
memoizedState: hook.memoizedState,
323456

324-
baseState: hook.baseState,
325-
queue: hook.queue,
326-
baseUpdate: hook.baseUpdate,
457+
baseState: hook.baseState,
458+
queue: hook.queue,
459+
baseUpdate: hook.baseUpdate,
327460

328-
next: null,
329-
};
461+
next: null,
462+
}
463+
: {
464+
memoizedState: hook.memoizedState,
465+
466+
baseState: hook.baseState,
467+
queue: hook.queue,
468+
baseUpdate: hook.baseUpdate,
469+
470+
next: null,
471+
};
472+
473+
if (__DEV__) {
474+
if (!currentHookMismatch) {
475+
if (currentHookNameInDev !== ((hook: any): HookDev)._debugType) {
476+
currentHookMismatch = new Error('tracer').stack
477+
.split('\n')
478+
.slice(4)
479+
.join('\n');
480+
}
481+
}
482+
}
483+
return nextHook;
330484
}
331485

332486
function createWorkInProgressHook(): Hook {
@@ -390,6 +544,8 @@ export function useContext<T>(
390544
): T {
391545
if (__DEV__) {
392546
currentHookNameInDev = 'useContext';
547+
createWorkInProgressHook();
548+
currentHookNameInDev = null;
393549
}
394550
// Ensure we're in a function component (class components support only the
395551
// .unstable_read() form)
@@ -422,6 +578,7 @@ export function useReducer<S, A>(
422578
}
423579
let fiber = (currentlyRenderingFiber = resolveCurrentlyRenderingFiber());
424580
workInProgressHook = createWorkInProgressHook();
581+
currentHookNameInDev = null;
425582
let queue: UpdateQueue<S, A> | null = (workInProgressHook.queue: any);
426583
if (queue !== null) {
427584
// Already have a queue, so this is an update.
@@ -600,7 +757,11 @@ function pushEffect(tag, create, destroy, deps) {
600757

601758
export function useRef<T>(initialValue: T): {current: T} {
602759
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
760+
if (__DEV__) {
761+
currentHookNameInDev = 'useRef';
762+
}
603763
workInProgressHook = createWorkInProgressHook();
764+
currentHookNameInDev = null;
604765
let ref;
605766

606767
if (workInProgressHook.memoizedState === null) {
@@ -620,7 +781,9 @@ export function useLayoutEffect(
620781
deps: Array<mixed> | void | null,
621782
): void {
622783
if (__DEV__) {
623-
currentHookNameInDev = 'useLayoutEffect';
784+
if (currentHookNameInDev !== 'useImperativeHandle') {
785+
currentHookNameInDev = 'useLayoutEffect';
786+
}
624787
}
625788
useEffectImpl(UpdateEffect, UnmountMutation | MountLayout, create, deps);
626789
}
@@ -653,6 +816,7 @@ function useEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
653816
const prevDeps = prevEffect.deps;
654817
if (areHookInputsEqual(nextDeps, prevDeps)) {
655818
pushEffect(NoHookEffect, create, destroy, nextDeps);
819+
currentHookNameInDev = null;
656820
return;
657821
}
658822
}
@@ -665,6 +829,7 @@ function useEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
665829
destroy,
666830
nextDeps,
667831
);
832+
currentHookNameInDev = null;
668833
}
669834

670835
export function useImperativeHandle<T>(
@@ -746,11 +911,13 @@ export function useCallback<T>(
746911
if (nextDeps !== null) {
747912
const prevDeps: Array<mixed> | null = prevState[1];
748913
if (areHookInputsEqual(nextDeps, prevDeps)) {
914+
currentHookNameInDev = null;
749915
return prevState[0];
750916
}
751917
}
752918
}
753919
workInProgressHook.memoizedState = [callback, nextDeps];
920+
currentHookNameInDev = null;
754921
return callback;
755922
}
756923

@@ -772,6 +939,7 @@ export function useMemo<T>(
772939
if (nextDeps !== null) {
773940
const prevDeps: Array<mixed> | null = prevState[1];
774941
if (areHookInputsEqual(nextDeps, prevDeps)) {
942+
currentHookNameInDev = null;
775943
return prevState[0];
776944
}
777945
}
@@ -782,6 +950,7 @@ export function useMemo<T>(
782950
const nextValue = nextCreate();
783951
currentlyRenderingFiber = fiber;
784952
workInProgressHook.memoizedState = [nextValue, nextDeps];
953+
currentHookNameInDev = null;
785954
return nextValue;
786955
}
787956

0 commit comments

Comments
 (0)