@@ -54,6 +54,27 @@ type UpdateQueue<S, A> = {
54
54
eagerState : S | null ,
55
55
} ;
56
56
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
+
57
78
export type Hook = {
58
79
memoizedState : any ,
59
80
@@ -64,6 +85,10 @@ export type Hook = {
64
85
next : Hook | null ,
65
86
} ;
66
87
88
+ type HookDev = Hook & {
89
+ _debugType : HookType ,
90
+ } ;
91
+
67
92
type Effect = {
68
93
tag : HookEffectTag ,
69
94
create : ( ) => mixed ,
@@ -118,7 +143,7 @@ let numberOfReRenders: number = -1;
118
143
const RE_RENDER_LIMIT = 25 ;
119
144
120
145
// In DEV, this is the name of the currently executing primitive hook
121
- let currentHookNameInDev : ?string ;
146
+ let currentHookNameInDev : ?HookType = null ;
122
147
123
148
function resolveCurrentlyRenderingFiber ( ) : Fiber {
124
149
invariant (
@@ -170,6 +195,95 @@ function areHookInputsEqual(
170
195
return true ;
171
196
}
172
197
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
+
173
287
export function renderWithHooks (
174
288
current : Fiber | null ,
175
289
workInProgress : Fiber ,
@@ -221,6 +335,7 @@ export function renderWithHooks(
221
335
getComponentName ( Component ) ,
222
336
) ;
223
337
}
338
+ flushHookMismatchWarnings ( ) ;
224
339
}
225
340
} while ( didScheduleRenderPhaseUpdate ) ;
226
341
@@ -248,7 +363,7 @@ export function renderWithHooks(
248
363
componentUpdateQueue = null ;
249
364
250
365
if ( __DEV__ ) {
251
- currentHookNameInDev = undefined ;
366
+ currentHookNameInDev = null ;
252
367
}
253
368
254
369
// These were reset above
@@ -281,6 +396,9 @@ export function resetHooks(): void {
281
396
if ( ! enableHooks ) {
282
397
return ;
283
398
}
399
+ if ( __DEV__ ) {
400
+ flushHookMismatchWarnings ( ) ;
401
+ }
284
402
285
403
// This is used to reset the state of this module when a component throws.
286
404
// It's also called inside mountIndeterminateComponent if we determine the
@@ -297,7 +415,7 @@ export function resetHooks(): void {
297
415
componentUpdateQueue = null ;
298
416
299
417
if ( __DEV__ ) {
300
- currentHookNameInDev = undefined ;
418
+ currentHookNameInDev = null ;
301
419
}
302
420
303
421
didScheduleRenderPhaseUpdate = false ;
@@ -306,27 +424,63 @@ export function resetHooks(): void {
306
424
}
307
425
308
426
function createHook ( ) : Hook {
309
- return {
310
- memoizedState : null ,
427
+ let hook : Hook = __DEV__
428
+ ? {
429
+ _debugType : ( ( currentHookNameInDev : any ) : HookType ) ,
430
+ memoizedState : null ,
311
431
312
- baseState : null ,
313
- queue : null ,
314
- baseUpdate : null ,
432
+ baseState : null ,
433
+ queue : null ,
434
+ baseUpdate : null ,
315
435
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 ;
318
449
}
319
450
320
451
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 ,
323
456
324
- baseState : hook . baseState ,
325
- queue : hook . queue ,
326
- baseUpdate : hook . baseUpdate ,
457
+ baseState : hook . baseState ,
458
+ queue : hook . queue ,
459
+ baseUpdate : hook . baseUpdate ,
327
460
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 ;
330
484
}
331
485
332
486
function createWorkInProgressHook ( ) : Hook {
@@ -390,6 +544,8 @@ export function useContext<T>(
390
544
): T {
391
545
if ( __DEV__ ) {
392
546
currentHookNameInDev = 'useContext' ;
547
+ createWorkInProgressHook ( ) ;
548
+ currentHookNameInDev = null ;
393
549
}
394
550
// Ensure we're in a function component (class components support only the
395
551
// .unstable_read() form)
@@ -422,6 +578,7 @@ export function useReducer<S, A>(
422
578
}
423
579
let fiber = ( currentlyRenderingFiber = resolveCurrentlyRenderingFiber ( ) ) ;
424
580
workInProgressHook = createWorkInProgressHook ( ) ;
581
+ currentHookNameInDev = null ;
425
582
let queue : UpdateQueue < S , A > | null = (workInProgressHook.queue: any);
426
583
if (queue !== null) {
427
584
// Already have a queue, so this is an update.
@@ -600,7 +757,11 @@ function pushEffect(tag, create, destroy, deps) {
600
757
601
758
export function useRef < T > (initialValue: T): { current : T } {
602
759
currentlyRenderingFiber = resolveCurrentlyRenderingFiber ( ) ;
760
+ if ( __DEV__ ) {
761
+ currentHookNameInDev = 'useRef' ;
762
+ }
603
763
workInProgressHook = createWorkInProgressHook();
764
+ currentHookNameInDev = null;
604
765
let ref;
605
766
606
767
if (workInProgressHook.memoizedState === null) {
@@ -620,7 +781,9 @@ export function useLayoutEffect(
620
781
deps : Array < mixed > | void | null,
621
782
): void {
622
783
if ( __DEV__ ) {
623
- currentHookNameInDev = 'useLayoutEffect' ;
784
+ if ( currentHookNameInDev !== 'useImperativeHandle' ) {
785
+ currentHookNameInDev = 'useLayoutEffect' ;
786
+ }
624
787
}
625
788
useEffectImpl ( UpdateEffect , UnmountMutation | MountLayout , create , deps ) ;
626
789
}
@@ -653,6 +816,7 @@ function useEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
653
816
const prevDeps = prevEffect . deps ;
654
817
if ( areHookInputsEqual ( nextDeps , prevDeps ) ) {
655
818
pushEffect ( NoHookEffect , create , destroy , nextDeps ) ;
819
+ currentHookNameInDev = null ;
656
820
return ;
657
821
}
658
822
}
@@ -665,6 +829,7 @@ function useEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
665
829
destroy ,
666
830
nextDeps ,
667
831
) ;
832
+ currentHookNameInDev = null ;
668
833
}
669
834
670
835
export function useImperativeHandle < T > (
@@ -746,11 +911,13 @@ export function useCallback<T>(
746
911
if ( nextDeps !== null ) {
747
912
const prevDeps : Array < mixed > | null = prevState [ 1 ] ;
748
913
if ( areHookInputsEqual ( nextDeps , prevDeps ) ) {
914
+ currentHookNameInDev = null ;
749
915
return prevState [ 0 ] ;
750
916
}
751
917
}
752
918
}
753
919
workInProgressHook.memoizedState = [callback, nextDeps];
920
+ currentHookNameInDev = null;
754
921
return callback;
755
922
}
756
923
@@ -772,6 +939,7 @@ export function useMemo<T>(
772
939
if ( nextDeps !== null ) {
773
940
const prevDeps : Array < mixed > | null = prevState [ 1 ] ;
774
941
if ( areHookInputsEqual ( nextDeps , prevDeps ) ) {
942
+ currentHookNameInDev = null ;
775
943
return prevState [ 0 ] ;
776
944
}
777
945
}
@@ -782,6 +950,7 @@ export function useMemo<T>(
782
950
const nextValue = nextCreate();
783
951
currentlyRenderingFiber = fiber;
784
952
workInProgressHook.memoizedState = [nextValue, nextDeps];
953
+ currentHookNameInDev = null;
785
954
return nextValue;
786
955
}
787
956
0 commit comments