Skip to content

Commit 70d4075

Browse files
authored
Move Hook mismatch warning to first mismatch site (#14720)
* Move Hook mismatch warning to first mismatch site Allows us to localize the warning logic in one place. * Nit
1 parent ba6477a commit 70d4075

File tree

2 files changed

+63
-107
lines changed

2 files changed

+63
-107
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

+57-105
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,6 @@ type HookType =
9898
| 'useImperativeHandle'
9999
| 'useDebugValue';
100100

101-
// the first instance of a hook mismatch in a component,
102-
// represented by a portion of its stacktrace
103-
let currentHookMismatchInDev = null;
104-
105101
let didWarnAboutMismatchedHooksForComponent;
106102
if (__DEV__) {
107103
didWarnAboutMismatchedHooksForComponent = new Set();
@@ -180,6 +176,56 @@ const RE_RENDER_LIMIT = 25;
180176
// In DEV, this is the name of the currently executing primitive hook
181177
let currentHookNameInDev: ?HookType = null;
182178

179+
function warnOnHookMismatchInDev() {
180+
if (__DEV__) {
181+
const componentName = getComponentName(
182+
((currentlyRenderingFiber: any): Fiber).type,
183+
);
184+
if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) {
185+
didWarnAboutMismatchedHooksForComponent.add(componentName);
186+
187+
const secondColumnStart = 22;
188+
189+
let table = '';
190+
let prevHook: HookDev | null = (firstCurrentHook: any);
191+
let nextHook: HookDev | null = (firstWorkInProgressHook: any);
192+
let n = 1;
193+
while (prevHook !== null && nextHook !== null) {
194+
const oldHookName = prevHook._debugType;
195+
const newHookName = nextHook._debugType;
196+
197+
let row = `${n}. ${oldHookName}`;
198+
199+
// Extra space so second column lines up
200+
// lol @ IE not supporting String#repeat
201+
while (row.length < secondColumnStart) {
202+
row += ' ';
203+
}
204+
205+
row += newHookName + '\n';
206+
207+
table += row;
208+
prevHook = (prevHook.next: any);
209+
nextHook = (nextHook.next: any);
210+
n++;
211+
}
212+
213+
warning(
214+
false,
215+
'React has detected a change in the order of Hooks called by %s. ' +
216+
'This will lead to bugs and errors if not fixed. ' +
217+
'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
218+
' Previous render Next render\n' +
219+
' -------------------------------\n' +
220+
'%s' +
221+
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
222+
componentName,
223+
table,
224+
);
225+
}
226+
}
227+
}
228+
183229
function throwInvalidHookError() {
184230
invariant(
185231
false,
@@ -229,90 +275,6 @@ function areHookInputsEqual(
229275
return true;
230276
}
231277

232-
function flushHookMismatchWarnings() {
233-
// we'll show the diff of the low level hooks,
234-
// and a stack trace so the dev can locate where
235-
// the first mismatch is coming from
236-
if (__DEV__) {
237-
if (currentHookMismatchInDev !== null) {
238-
let componentName = getComponentName(
239-
((currentlyRenderingFiber: any): Fiber).type,
240-
);
241-
if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) {
242-
didWarnAboutMismatchedHooksForComponent.add(componentName);
243-
const hookStackDiff = [];
244-
let current = firstCurrentHook;
245-
const previousOrder = [];
246-
while (current !== null) {
247-
previousOrder.push(((current: any): HookDev)._debugType);
248-
current = current.next;
249-
}
250-
let workInProgress = firstWorkInProgressHook;
251-
const nextOrder = [];
252-
while (workInProgress !== null) {
253-
nextOrder.push(((workInProgress: any): HookDev)._debugType);
254-
workInProgress = workInProgress.next;
255-
}
256-
// some bookkeeping for formatting the output table
257-
const columnLength = Math.max.apply(
258-
null,
259-
previousOrder
260-
.map(hook => hook.length)
261-
.concat(' Previous render'.length),
262-
);
263-
264-
const padEndSpaces = (string, length) => {
265-
if (string.length >= length) {
266-
return string;
267-
}
268-
return string + ' ' + new Array(length - string.length).join(' ');
269-
};
270-
271-
let hookStackHeader =
272-
((padEndSpaces(' Previous render', columnLength): any): string) +
273-
' Next render\n';
274-
const hookStackWidth = hookStackHeader.length;
275-
hookStackHeader += ' ' + new Array(hookStackWidth - 2).join('-');
276-
const hookStackFooter = ' ' + new Array(hookStackWidth - 2).join('^');
277-
278-
const hookStackLength = Math.max(
279-
previousOrder.length,
280-
nextOrder.length,
281-
);
282-
for (let i = 0; i < hookStackLength; i++) {
283-
hookStackDiff.push(
284-
((padEndSpaces(i + 1 + '. ', 3): any): string) +
285-
((padEndSpaces(previousOrder[i], columnLength): any): string) +
286-
' ' +
287-
nextOrder[i],
288-
);
289-
if (previousOrder[i] !== nextOrder[i]) {
290-
break;
291-
}
292-
}
293-
warning(
294-
false,
295-
'React has detected a change in the order of Hooks called by %s. ' +
296-
'This will lead to bugs and errors if not fixed. ' +
297-
'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
298-
'%s\n' +
299-
'%s\n' +
300-
'%s\n' +
301-
'The first Hook type mismatch occured at:\n' +
302-
'%s\n\n' +
303-
'This error occurred in the following component:',
304-
componentName,
305-
hookStackHeader,
306-
hookStackDiff.join('\n'),
307-
hookStackFooter,
308-
currentHookMismatchInDev,
309-
);
310-
}
311-
currentHookMismatchInDev = null;
312-
}
313-
}
314-
}
315-
316278
export function renderWithHooks(
317279
current: Fiber | null,
318280
workInProgress: Fiber,
@@ -378,7 +340,6 @@ export function renderWithHooks(
378340
}
379341

380342
if (__DEV__) {
381-
flushHookMismatchWarnings();
382343
currentHookNameInDev = null;
383344
}
384345

@@ -437,10 +398,6 @@ export function bailoutHooks(
437398
}
438399

439400
export function resetHooks(): void {
440-
if (__DEV__) {
441-
flushHookMismatchWarnings();
442-
}
443-
444401
// We can assume the previous dispatcher is always this one, since we set it
445402
// at the beginning of the render phase and there's no re-entrancy.
446403
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
@@ -537,18 +494,6 @@ function updateWorkInProgressHook(): Hook {
537494
next: null,
538495
};
539496

540-
if (__DEV__) {
541-
(newHook: any)._debugType = (currentHookNameInDev: any);
542-
if (currentHookMismatchInDev === null) {
543-
if (currentHookNameInDev !== ((currentHook: any): HookDev)._debugType) {
544-
currentHookMismatchInDev = new Error('tracer').stack
545-
.split('\n')
546-
.slice(4)
547-
.join('\n');
548-
}
549-
}
550-
}
551-
552497
if (workInProgressHook === null) {
553498
// This is the first hook in the list.
554499
workInProgressHook = firstWorkInProgressHook = newHook;
@@ -557,6 +502,13 @@ function updateWorkInProgressHook(): Hook {
557502
workInProgressHook = workInProgressHook.next = newHook;
558503
}
559504
nextCurrentHook = currentHook.next;
505+
506+
if (__DEV__) {
507+
(newHook: any)._debugType = (currentHookNameInDev: any);
508+
if (currentHookNameInDev !== ((currentHook: any): HookDev)._debugType) {
509+
warnOnHookMismatchInDev();
510+
}
511+
}
560512
}
561513
return workInProgressHook;
562514
}

packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -1159,7 +1159,7 @@ describe('ReactHooks', () => {
11591159
});
11601160

11611161
it('warns on using differently ordered hooks on subsequent renders', () => {
1162-
const {useState, useReducer} = React;
1162+
const {useState, useReducer, useRef} = React;
11631163
function useCustomHook() {
11641164
return useState(0);
11651165
}
@@ -1172,6 +1172,9 @@ describe('ReactHooks', () => {
11721172
useReducer((s, a) => a, 0);
11731173
useCustomHook(0);
11741174
}
1175+
// This should not appear in the warning message because it occurs after
1176+
// the first mismatch
1177+
const ref = useRef(null);
11751178
return null;
11761179
/* eslint-enable no-unused-vars */
11771180
}
@@ -1185,7 +1188,8 @@ describe('ReactHooks', () => {
11851188
' Previous render Next render\n' +
11861189
' -------------------------------\n' +
11871190
'1. useReducer useState\n' +
1188-
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
1191+
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
1192+
' in App (at **)',
11891193
]);
11901194

11911195
// further warnings for this component are silenced

0 commit comments

Comments
 (0)