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

2 files changed

Lines changed: 63 additions & 107 deletions

File tree

‎packages/react-reconciler/src/ReactFiberHooks.js‎

Lines changed: 57 additions & 105 deletions
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-
letcurrentHookMismatchInDev=null;
104-
105101
letdidWarnAboutMismatchedHooksForComponent;
106102
if(__DEV__){
107103
didWarnAboutMismatchedHooksForComponent =newSet();
@@ -180,6 +176,56 @@ const RE_RENDER_LIMIT = 25;
180176
// In DEV, this is the name of the currently executing primitive hook
181177
letcurrentHookNameInDev: ?HookType=null;
182178

179+
functionwarnOnHookMismatchInDev(){
180+
if(__DEV__){
181+
constcomponentName=getComponentName(
182+
((currentlyRenderingFiber: any): Fiber).type,
183+
);
184+
if(!didWarnAboutMismatchedHooksForComponent.has(componentName)){
185+
didWarnAboutMismatchedHooksForComponent.add(componentName);
186+
187+
constsecondColumnStart=22;
188+
189+
lettable='';
190+
letprevHook: HookDev|null=(firstCurrentHook: any);
191+
letnextHook: HookDev|null=(firstWorkInProgressHook: any);
192+
letn=1;
193+
while(prevHook!==null&&nextHook!==null){
194+
constoldHookName=prevHook._debugType;
195+
constnewHookName=nextHook._debugType;
196+
197+
letrow=`${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
functionthrowInvalidHookError(){
184230
invariant(
185231
false,
@@ -229,90 +275,6 @@ function areHookInputsEqual(
229275
returntrue;
230276
}
231277

232-
functionflushHookMismatchWarnings(){
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-
letcomponentName=getComponentName(
239-
((currentlyRenderingFiber: any): Fiber).type,
240-
);
241-
if(!didWarnAboutMismatchedHooksForComponent.has(componentName)){
242-
didWarnAboutMismatchedHooksForComponent.add(componentName);
243-
consthookStackDiff=[];
244-
letcurrent=firstCurrentHook;
245-
constpreviousOrder=[];
246-
while(current!==null){
247-
previousOrder.push(((current: any): HookDev)._debugType);
248-
current=current.next;
249-
}
250-
letworkInProgress=firstWorkInProgressHook;
251-
constnextOrder=[];
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-
constcolumnLength=Math.max.apply(
258-
null,
259-
previousOrder
260-
.map(hook=>hook.length)
261-
.concat(' Previous render'.length),
262-
);
263-
264-
constpadEndSpaces=(string,length)=>{
265-
if(string.length>=length){
266-
returnstring;
267-
}
268-
returnstring+' '+newArray(length-string.length).join(' ');
269-
};
270-
271-
lethookStackHeader=
272-
((padEndSpaces(' Previous render',columnLength): any): string)+
273-
' Next render\n';
274-
consthookStackWidth=hookStackHeader.length;
275-
hookStackHeader+=' '+newArray(hookStackWidth-2).join('-');
276-
consthookStackFooter=' '+newArray(hookStackWidth-2).join('^');
277-
278-
consthookStackLength=Math.max(
279-
previousOrder.length,
280-
nextOrder.length,
281-
);
282-
for(leti=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 functionrenderWithHooks(
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
exportfunctionresetHooks(): 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=newError('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
returnworkInProgressHook;
562514
}

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

Lines changed: 6 additions & 2 deletions
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
functionuseCustomHook(){
11641164
returnuseState(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+
constref=useRef(null);
11751178
returnnull;
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)