@@ -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+
5778export 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+
6792type Effect = {
6893 tag : HookEffectTag ,
6994 create : ( ) => mixed ,
@@ -118,7 +143,7 @@ let numberOfReRenders: number = -1;
118143const 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
123148function 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+
173287export 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
308426function 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
320451function 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
332486function 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
601758export 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
670835export 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