From 1975f38e2800586ee7cce4bc87808634b8427e6a Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 24 Feb 2019 08:37:43 -0800 Subject: [PATCH 1/5] ReactDebugHooks useContext() advances hooks list in DEV mode --- .../react-debug-tools/src/ReactDebugHooks.js | 4 +++ .../ReactHooksInspectionIntegration-test.js | 27 +++++++++++++++++++ .../react-reconciler/src/ReactFiberHooks.js | 2 ++ 3 files changed, 33 insertions(+) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 2dc1ddb02cac..03628a6e3bc9 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -93,6 +93,10 @@ function useContext( context: ReactContext, observedBits: void | number | boolean, ): T { + if (__DEV__) { + // ReactFiberHooks only adds context to the hooks list in DEV. + nextHook(); + } hookLog.push({ primitive: 'Context', stackError: new Error(), diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 3c7972aff4df..730ed9435b70 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -427,4 +427,31 @@ describe('ReactHooksInspectionIntegration', () => { expect(setterCalls[0]).not.toBe(initial); expect(setterCalls[1]).toBe(initial); }); + + // This test case is based on an open source bug report: + // facebookincubator/redux-react-hook/issues/34#issuecomment-466693787 + it('should properly advance the current hook for useContext', () => { + const MyContext = React.createContext(123); + + let hasInitializedState = false; + const initializeStateOnce = () => { + if (hasInitializedState) { + throw Error( + 'State initialization function should only be called once.', + ); + } + hasInitializedState = true; + return {foo: 'abc'}; + }; + + function Foo(props) { + React.useContext(MyContext); + const [data] = React.useState(initializeStateOnce); + return
foo: {data.foo}
; + } + + const renderer = ReactTestRenderer.create(); + const childFiber = renderer.root._currentFiber(); + ReactDebugTools.inspectHooksOfFiber(childFiber); + }); }); diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 702814f9c449..e312b8ee81df 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -525,6 +525,7 @@ function mountContext( observedBits: void | number | boolean, ): T { if (__DEV__) { + // If this DEV conditional is ever removed, update ReactDebugHooks useContext too. mountWorkInProgressHook(); } return readContext(context, observedBits); @@ -535,6 +536,7 @@ function updateContext( observedBits: void | number | boolean, ): T { if (__DEV__) { + // If this DEV conditional is ever removed, update ReactDebugHooks useContext too. updateWorkInProgressHook(); } return readContext(context, observedBits); From 842d13572d2a851496b9c9674fb3e2a00cfdb1a1 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 24 Feb 2019 09:31:01 -0800 Subject: [PATCH 2/5] Don't bypass useContext to go straight to readContext anymore This adds a small amount of overhead, so I'm not sure if it will fly. I think it might be necessary though in order to support the react-debug-tools package. --- .../react-debug-tools/src/ReactDebugHooks.js | 5 +-- .../ReactHooksInspectionIntegration-test.js | 43 ++++++++++++------- .../react-reconciler/src/ReactFiberHooks.js | 14 ++---- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 03628a6e3bc9..bec6bc2fe9bc 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -93,10 +93,7 @@ function useContext( context: ReactContext, observedBits: void | number | boolean, ): T { - if (__DEV__) { - // ReactFiberHooks only adds context to the hooks list in DEV. - nextHook(); - } + nextHook(); hookLog.push({ primitive: 'Context', stackError: new Error(), diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 730ed9435b70..8c56c8cd775a 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -431,27 +431,38 @@ describe('ReactHooksInspectionIntegration', () => { // This test case is based on an open source bug report: // facebookincubator/redux-react-hook/issues/34#issuecomment-466693787 it('should properly advance the current hook for useContext', () => { - const MyContext = React.createContext(123); - - let hasInitializedState = false; - const initializeStateOnce = () => { - if (hasInitializedState) { - throw Error( - 'State initialization function should only be called once.', - ); - } - hasInitializedState = true; - return {foo: 'abc'}; - }; + const MyContext = React.createContext(1); + + let incrementCount; function Foo(props) { - React.useContext(MyContext); - const [data] = React.useState(initializeStateOnce); - return
foo: {data.foo}
; + const context = React.useContext(MyContext); + const [data, setData] = React.useState({count: context}); + + incrementCount = () => setData(({count}) => ({count: count + 1})); + + return
count: {data.count}
; } const renderer = ReactTestRenderer.create(); + expect(renderer.toJSON()).toEqual({ + type: 'div', + props: {}, + children: ['count: ', '1'], + }); + + act(incrementCount); + expect(renderer.toJSON()).toEqual({ + type: 'div', + props: {}, + children: ['count: ', '2'], + }); + const childFiber = renderer.root._currentFiber(); - ReactDebugTools.inspectHooksOfFiber(childFiber); + const tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + {name: 'Context', value: 1, subHooks: []}, + {name: 'State', value: {count: 2}, subHooks: []}, + ]); }); }); diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index e312b8ee81df..02f416e80881 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -524,10 +524,7 @@ function mountContext( context: ReactContext, observedBits: void | number | boolean, ): T { - if (__DEV__) { - // If this DEV conditional is ever removed, update ReactDebugHooks useContext too. - mountWorkInProgressHook(); - } + mountWorkInProgressHook(); return readContext(context, observedBits); } @@ -535,10 +532,7 @@ function updateContext( context: ReactContext, observedBits: void | number | boolean, ): T { - if (__DEV__) { - // If this DEV conditional is ever removed, update ReactDebugHooks useContext too. - updateWorkInProgressHook(); - } + updateWorkInProgressHook(); return readContext(context, observedBits); } @@ -1156,7 +1150,7 @@ const HooksDispatcherOnMount: Dispatcher = { readContext, useCallback: mountCallback, - useContext: readContext, + useContext: mountContext, useEffect: mountEffect, useImperativeHandle: mountImperativeHandle, useLayoutEffect: mountLayoutEffect, @@ -1171,7 +1165,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { readContext, useCallback: updateCallback, - useContext: readContext, + useContext: updateContext, useEffect: updateEffect, useImperativeHandle: updateImperativeHandle, useLayoutEffect: updateLayoutEffect, From 9bbd4f4fed844545d4664d92d6f573e3669cbf5c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 25 Feb 2019 15:00:12 -0800 Subject: [PATCH 3/5] Refactor hook ordering check to use DEV-only data structure This enables us to warn about more cases (e.g. useContext, useDebugValue) withou the need to add any overhead to production bundles. --- .../react-debug-tools/src/ReactDebugHooks.js | 1 - packages/react-reconciler/src/ReactFiber.js | 7 + .../react-reconciler/src/ReactFiberHooks.js | 233 ++++++++----- .../src/__tests__/ReactHooks-test.internal.js | 319 ++++++++++++++---- ...eactHooksWithNoopRenderer-test.internal.js | 31 +- 5 files changed, 434 insertions(+), 157 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index bec6bc2fe9bc..2dc1ddb02cac 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -93,7 +93,6 @@ function useContext( context: ReactContext, observedBits: void | number | boolean, ): T { - nextHook(); hookLog.push({ primitive: 'Context', stackError: new Error(), diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 681e3e419e13..08bff4899e9e 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -15,6 +15,7 @@ import type {SideEffectTag} from 'shared/ReactSideEffectTags'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {UpdateQueue} from './ReactUpdateQueue'; import type {ContextDependencyList} from './ReactFiberNewContext'; +import type {HookType} from './ReactFiberHooks'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; @@ -204,6 +205,9 @@ export type Fiber = {| _debugSource?: Source | null, _debugOwner?: Fiber | null, _debugIsCurrentlyTiming?: boolean, + + // Used to verify that the order of hooks does not change between renders. + _debugHookTypes?: Array | null, |}; let debugCounter; @@ -285,6 +289,7 @@ function FiberNode( this._debugSource = null; this._debugOwner = null; this._debugIsCurrentlyTiming = false; + this._debugHookTypes = null; if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') { Object.preventExtensions(this); } @@ -370,6 +375,7 @@ export function createWorkInProgress( workInProgress._debugID = current._debugID; workInProgress._debugSource = current._debugSource; workInProgress._debugOwner = current._debugOwner; + workInProgress._debugHookTypes = current._debugHookTypes; } workInProgress.alternate = current; @@ -723,5 +729,6 @@ export function assignFiberPropertiesInDEV( target._debugSource = source._debugSource; target._debugOwner = source._debugOwner; target._debugIsCurrentlyTiming = source._debugIsCurrentlyTiming; + target._debugHookTypes = source._debugHookTypes; return target; } diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 02f416e80881..86c0b8c37a2b 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -93,7 +93,7 @@ type UpdateQueue = { eagerState: S | null, }; -type HookType = +export type HookType = | 'useState' | 'useReducer' | 'useContext' @@ -120,10 +120,6 @@ export type Hook = { next: Hook | null, }; -type HookDev = Hook & { - _debugType: HookType, -}; - type Effect = { tag: HookEffectTag, create: () => (() => void) | void, @@ -150,7 +146,6 @@ let currentlyRenderingFiber: Fiber | null = null; // current hook list is the list that belongs to the current fiber. The // work-in-progress hook list is a new list that will be added to the // work-in-progress fiber. -let firstCurrentHook: Hook | null = null; let currentHook: Hook | null = null; let nextCurrentHook: Hook | null = null; let firstWorkInProgressHook: Hook | null = null; @@ -183,7 +178,33 @@ const RE_RENDER_LIMIT = 25; // In DEV, this is the name of the currently executing primitive hook let currentHookNameInDev: ?HookType = null; -function warnOnHookMismatchInDev() { +// In DEV, this list ensures that hooks are called in the same order between renders. +// The list stores the order of hooks used during the initial render (mount). +// Subsequent renders (updates) reference this list. +let hookTypesDev: Array | null = null; +let hookTypesUpdateIndexDev: number = -1; + +function mountHookTypeDev(hookName: HookType) { + if (__DEV__) { + if (hookTypesDev === null) { + hookTypesDev = [hookName]; + } else { + hookTypesDev.push(hookName); + } + } +} + +function updateHookTypeDev(hookName: HookType) { + if (__DEV__) { + if (hookTypesDev !== null) { + if (hookTypesDev[++hookTypesUpdateIndexDev] !== hookName) { + warnOnHookMismatchInDev(hookName); + } + } + } +} + +function warnOnHookMismatchInDev(currentHookName: HookType) { if (__DEV__) { const componentName = getComponentName( ((currentlyRenderingFiber: any): Fiber).type, @@ -191,44 +212,42 @@ function warnOnHookMismatchInDev() { if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) { didWarnAboutMismatchedHooksForComponent.add(componentName); - const secondColumnStart = 22; + if (hookTypesDev !== null) { + let table = ''; - let table = ''; - let prevHook: HookDev | null = (firstCurrentHook: any); - let nextHook: HookDev | null = (firstWorkInProgressHook: any); - let n = 1; - while (prevHook !== null && nextHook !== null) { - const oldHookName = prevHook._debugType; - const newHookName = nextHook._debugType; + const secondColumnStart = 30; - let row = `${n}. ${oldHookName}`; + for (let i = 0; i <= hookTypesUpdateIndexDev; i++) { + const oldHookName = hookTypesDev[i]; + const newHookName = + i === hookTypesUpdateIndexDev ? currentHookName : oldHookName; - // Extra space so second column lines up - // lol @ IE not supporting String#repeat - while (row.length < secondColumnStart) { - row += ' '; - } + let row = `${i + 1}. ${oldHookName}`; + + // Extra space so second column lines up + // lol @ IE not supporting String#repeat + while (row.length < secondColumnStart) { + row += ' '; + } - row += newHookName + '\n'; + row += newHookName + '\n'; - table += row; - prevHook = (prevHook.next: any); - nextHook = (nextHook.next: any); - n++; - } + table += row; + } - warning( - false, - 'React has detected a change in the order of Hooks called by %s. ' + - 'This will lead to bugs and errors if not fixed. ' + - 'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + - ' Previous render Next render\n' + - ' -------------------------------\n' + - '%s' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', - componentName, - table, - ); + warning( + false, + 'React has detected a change in the order of Hooks called by %s. ' + + 'This will lead to bugs and errors if not fixed. ' + + 'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '%s' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n', + componentName, + table, + ); + } } } } @@ -293,8 +312,15 @@ export function renderWithHooks( ): any { renderExpirationTime = nextRenderExpirationTime; currentlyRenderingFiber = workInProgress; - firstCurrentHook = nextCurrentHook = - current !== null ? current.memoizedState : null; + nextCurrentHook = current !== null ? current.memoizedState : null; + + if (__DEV__) { + hookTypesDev = + current !== null + ? ((current._debugHookTypes: any): Array) + : null; + hookTypesUpdateIndexDev = -1; + } // The following should have already been reset // currentHook = null; @@ -308,12 +334,21 @@ export function renderWithHooks( // numberOfReRenders = 0; // sideEffectTag = 0; + // TODO Warn if no hooks are used at all during mount, then some are used during update. + // Currently we will identify the update render as a mount because nextCurrentHook === null. + // This is tricky because it's valid for certain types of components (e.g. React.lazy) + if (__DEV__) { ReactCurrentDispatcher.current = - nextCurrentHook === null + hookTypesDev === null ? HooksDispatcherOnMountInDEV : HooksDispatcherOnUpdateInDEV; } else { + // TODO This check isn't always accurate. + // Not all hooks are added to the Fiber's list (e.g. context) + // so using a non-null current hook might indicate "mount" when it's really an "update". + // We don't have a better data structure to check in production bundles though. + ReactCurrentDispatcher.current = nextCurrentHook === null ? HooksDispatcherOnMount @@ -328,14 +363,17 @@ export function renderWithHooks( numberOfReRenders += 1; // Start over from the beginning of the list - firstCurrentHook = nextCurrentHook = - current !== null ? current.memoizedState : null; + nextCurrentHook = current !== null ? current.memoizedState : null; nextWorkInProgressHook = firstWorkInProgressHook; currentHook = null; workInProgressHook = null; componentUpdateQueue = null; + if (__DEV__) { + hookTypesUpdateIndexDev = -1; + } + ReactCurrentDispatcher.current = __DEV__ ? HooksDispatcherOnUpdateInDEV : HooksDispatcherOnUpdate; @@ -347,10 +385,6 @@ export function renderWithHooks( numberOfReRenders = 0; } - if (__DEV__) { - currentHookNameInDev = null; - } - // We can assume the previous dispatcher is always this one, since we set it // at the beginning of the render phase and there's no re-entrancy. ReactCurrentDispatcher.current = ContextOnlyDispatcher; @@ -362,19 +396,30 @@ export function renderWithHooks( renderedWork.updateQueue = (componentUpdateQueue: any); renderedWork.effectTag |= sideEffectTag; + if (__DEV__) { + renderedWork._debugHookTypes = hookTypesDev; + } + + // This check uses currentHook so that it works the same in DEV and prod bundles. + // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles. const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null; renderExpirationTime = NoWork; currentlyRenderingFiber = null; - firstCurrentHook = null; currentHook = null; nextCurrentHook = null; firstWorkInProgressHook = null; workInProgressHook = null; nextWorkInProgressHook = null; + if (__DEV__) { + currentHookNameInDev = null; + hookTypesDev = null; + hookTypesUpdateIndexDev = -1; + } + remainingExpirationTime = NoWork; componentUpdateQueue = null; sideEffectTag = 0; @@ -416,21 +461,23 @@ export function resetHooks(): void { renderExpirationTime = NoWork; currentlyRenderingFiber = null; - firstCurrentHook = null; currentHook = null; nextCurrentHook = null; firstWorkInProgressHook = null; workInProgressHook = null; nextWorkInProgressHook = null; - remainingExpirationTime = NoWork; - componentUpdateQueue = null; - sideEffectTag = 0; - if (__DEV__) { + hookTypesDev = null; + hookTypesUpdateIndexDev = -1; + currentHookNameInDev = null; } + remainingExpirationTime = NoWork; + componentUpdateQueue = null; + sideEffectTag = 0; + didScheduleRenderPhaseUpdate = false; renderPhaseUpdates = null; numberOfReRenders = 0; @@ -447,9 +494,6 @@ function mountWorkInProgressHook(): Hook { next: null, }; - if (__DEV__) { - (hook: any)._debugType = (currentHookNameInDev: any); - } if (workInProgressHook === null) { // This is the first hook in the list firstWorkInProgressHook = workInProgressHook = hook; @@ -499,13 +543,6 @@ function updateWorkInProgressHook(): Hook { workInProgressHook = workInProgressHook.next = newHook; } nextCurrentHook = currentHook.next; - - if (__DEV__) { - (newHook: any)._debugType = (currentHookNameInDev: any); - if (currentHookNameInDev !== ((currentHook: any): HookDev)._debugType) { - warnOnHookMismatchInDev(); - } - } } return workInProgressHook; } @@ -520,22 +557,6 @@ function basicStateReducer(state: S, action: BasicStateAction): S { return typeof action === 'function' ? action(state) : action; } -function mountContext( - context: ReactContext, - observedBits: void | number | boolean, -): T { - mountWorkInProgressHook(); - return readContext(context, observedBits); -} - -function updateContext( - context: ReactContext, - observedBits: void | number | boolean, -): T { - updateWorkInProgressHook(); - return readContext(context, observedBits); -} - function mountReducer( reducer: (S, A) => S, initialArg: I, @@ -1150,7 +1171,7 @@ const HooksDispatcherOnMount: Dispatcher = { readContext, useCallback: mountCallback, - useContext: mountContext, + useContext: readContext, useEffect: mountEffect, useImperativeHandle: mountImperativeHandle, useLayoutEffect: mountLayoutEffect, @@ -1165,7 +1186,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { readContext, useCallback: updateCallback, - useContext: updateContext, + useContext: readContext, useEffect: updateEffect, useImperativeHandle: updateImperativeHandle, useLayoutEffect: updateLayoutEffect, @@ -1212,6 +1233,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; + mountHookTypeDev(currentHookNameInDev); return mountCallback(callback, deps); }, useContext( @@ -1219,13 +1241,15 @@ if (__DEV__) { observedBits: void | number | boolean, ): T { currentHookNameInDev = 'useContext'; - return mountContext(context, observedBits); + mountHookTypeDev(currentHookNameInDev); + return readContext(context, observedBits); }, useEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { currentHookNameInDev = 'useEffect'; + mountHookTypeDev(currentHookNameInDev); return mountEffect(create, deps); }, useImperativeHandle( @@ -1234,6 +1258,7 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useImperativeHandle'; + mountHookTypeDev(currentHookNameInDev); return mountImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1241,10 +1266,12 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useLayoutEffect'; + mountHookTypeDev(currentHookNameInDev); return mountLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; + mountHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1259,6 +1286,7 @@ if (__DEV__) { init?: I => S, ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; + mountHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1269,12 +1297,14 @@ if (__DEV__) { }, useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; + mountHookTypeDev(currentHookNameInDev); return mountRef(initialValue); }, useState( initialState: (() => S) | S, ): [S, Dispatch>] { currentHookNameInDev = 'useState'; + mountHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1285,6 +1315,7 @@ if (__DEV__) { }, useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; + mountHookTypeDev(currentHookNameInDev); return mountDebugValue(value, formatterFn); }, }; @@ -1299,6 +1330,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; + updateHookTypeDev(currentHookNameInDev); return updateCallback(callback, deps); }, useContext( @@ -1306,13 +1338,15 @@ if (__DEV__) { observedBits: void | number | boolean, ): T { currentHookNameInDev = 'useContext'; - return updateContext(context, observedBits); + updateHookTypeDev(currentHookNameInDev); + return readContext(context, observedBits); }, useEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { currentHookNameInDev = 'useEffect'; + updateHookTypeDev(currentHookNameInDev); return updateEffect(create, deps); }, useImperativeHandle( @@ -1321,6 +1355,7 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useImperativeHandle'; + updateHookTypeDev(currentHookNameInDev); return updateImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1328,10 +1363,12 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useLayoutEffect'; + updateHookTypeDev(currentHookNameInDev); return updateLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; + updateHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1346,6 +1383,7 @@ if (__DEV__) { init?: I => S, ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; + updateHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1356,12 +1394,14 @@ if (__DEV__) { }, useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; + updateHookTypeDev(currentHookNameInDev); return updateRef(initialValue); }, useState( initialState: (() => S) | S, ): [S, Dispatch>] { currentHookNameInDev = 'useState'; + updateHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1372,6 +1412,7 @@ if (__DEV__) { }, useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; + updateHookTypeDev(currentHookNameInDev); return updateDebugValue(value, formatterFn); }, }; @@ -1388,6 +1429,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; warnInvalidHookAccess(); + mountHookTypeDev(currentHookNameInDev); return mountCallback(callback, deps); }, useContext( @@ -1396,7 +1438,8 @@ if (__DEV__) { ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); - return mountContext(context, observedBits); + mountHookTypeDev(currentHookNameInDev); + return readContext(context, observedBits); }, useEffect( create: () => (() => void) | void, @@ -1404,6 +1447,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useEffect'; warnInvalidHookAccess(); + mountHookTypeDev(currentHookNameInDev); return mountEffect(create, deps); }, useImperativeHandle( @@ -1413,6 +1457,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useImperativeHandle'; warnInvalidHookAccess(); + mountHookTypeDev(currentHookNameInDev); return mountImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1421,11 +1466,13 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useLayoutEffect'; warnInvalidHookAccess(); + mountHookTypeDev(currentHookNameInDev); return mountLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; warnInvalidHookAccess(); + mountHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1441,6 +1488,7 @@ if (__DEV__) { ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; warnInvalidHookAccess(); + mountHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1452,6 +1500,7 @@ if (__DEV__) { useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; warnInvalidHookAccess(); + mountHookTypeDev(currentHookNameInDev); return mountRef(initialValue); }, useState( @@ -1459,6 +1508,7 @@ if (__DEV__) { ): [S, Dispatch>] { currentHookNameInDev = 'useState'; warnInvalidHookAccess(); + mountHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1470,6 +1520,7 @@ if (__DEV__) { useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; warnInvalidHookAccess(); + mountHookTypeDev(currentHookNameInDev); return mountDebugValue(value, formatterFn); }, }; @@ -1486,6 +1537,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; warnInvalidHookAccess(); + updateHookTypeDev(currentHookNameInDev); return updateCallback(callback, deps); }, useContext( @@ -1494,7 +1546,8 @@ if (__DEV__) { ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); - return updateContext(context, observedBits); + updateHookTypeDev(currentHookNameInDev); + return readContext(context, observedBits); }, useEffect( create: () => (() => void) | void, @@ -1502,6 +1555,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useEffect'; warnInvalidHookAccess(); + updateHookTypeDev(currentHookNameInDev); return updateEffect(create, deps); }, useImperativeHandle( @@ -1511,6 +1565,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useImperativeHandle'; warnInvalidHookAccess(); + updateHookTypeDev(currentHookNameInDev); return updateImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1519,11 +1574,13 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useLayoutEffect'; warnInvalidHookAccess(); + updateHookTypeDev(currentHookNameInDev); return updateLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; warnInvalidHookAccess(); + updateHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1539,6 +1596,7 @@ if (__DEV__) { ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; warnInvalidHookAccess(); + updateHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1550,6 +1608,7 @@ if (__DEV__) { useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; warnInvalidHookAccess(); + updateHookTypeDev(currentHookNameInDev); return updateRef(initialValue); }, useState( @@ -1557,6 +1616,7 @@ if (__DEV__) { ): [S, Dispatch>] { currentHookNameInDev = 'useState'; warnInvalidHookAccess(); + updateHookTypeDev(currentHookNameInDev); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1568,6 +1628,7 @@ if (__DEV__) { useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; warnInvalidHookAccess(); + updateHookTypeDev(currentHookNameInDev); return updateDebugValue(value, formatterFn); }, }; diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index b7094beccd64..bcc3754bd19a 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -1002,7 +1002,7 @@ describe('ReactHooks', () => { }).toThrow('Rendered more hooks than during the previous render.'); if (__DEV__) { - expect(console.error).toHaveBeenCalledTimes(3); + expect(console.error).toHaveBeenCalledTimes(4); expect(console.error.calls.argsFor(0)[0]).toContain( 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', ); @@ -1337,74 +1337,261 @@ describe('ReactHooks', () => { expect(useMemoCount).toBe(__DEV__ ? 2 : 1); // Has Hooks }); - it('warns on using differently ordered hooks on subsequent renders', () => { - const {useState, useReducer, useRef} = React; - function useCustomHook() { - return useState(0); - } - function App(props) { - /* eslint-disable no-unused-vars */ - if (props.flip) { - useCustomHook(0); - useReducer((s, a) => a, 0); - } else { - useReducer((s, a) => a, 0); - useCustomHook(0); - } - // This should not appear in the warning message because it occurs after - // the first mismatch - const ref = useRef(null); - return null; - /* eslint-enable no-unused-vars */ - } - let root = ReactTestRenderer.create(); - expect(() => { - root.update(); - }).toWarnDev([ - 'Warning: React has detected a change in the order of Hooks called by App. ' + - 'This will lead to bugs and errors if not fixed. For more information, ' + - 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + - ' Previous render Next render\n' + - ' -------------------------------\n' + - '1. useReducer useState\n' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' + - ' in App (at **)', - ]); + describe('hook ordering', () => { + const useCallbackHelper = () => React.useCallback(() => {}, []); + const useContextHelper = () => React.useContext(React.createContext()); + const useDebugValueHelper = () => React.useDebugValue('abc'); + const useEffectHelper = () => React.useEffect(() => () => {}, []); + const useImperativeHandleHelper = () => { + React.useImperativeHandle({current: null}, () => ({}), []); + }; + const useLayoutEffectHelper = () => + React.useLayoutEffect(() => () => {}, []); + const useMemoHelper = () => React.useMemo(() => 123, []); + const useReducerHelper = () => React.useReducer((s, a) => a, 0); + const useRefHelper = () => React.useRef(null); + const useStateHelper = () => React.useState(0); + + // We don't include useImperativeHandleHelper in this set, + // because it generates an additional warning about the inputs length changing. + // We test it below with its own test. + let orderedHooks = [ + useCallbackHelper, + useContextHelper, + useDebugValueHelper, + useEffectHelper, + useLayoutEffectHelper, + useMemoHelper, + useReducerHelper, + useRefHelper, + useStateHelper, + ]; + + const formatHookNamesToMatchErrorMessage = (hookNameA, hookNameB) => { + return `use${hookNameA}${' '.repeat(24 - hookNameA.length)}${ + hookNameB ? `use${hookNameB}` : undefined + }`; + }; - // further warnings for this component are silenced - root.update(); - }); + orderedHooks.forEach((firstHelper, index) => { + const secondHelper = + index > 0 + ? orderedHooks[index - 1] + : orderedHooks[orderedHooks.length - 1]; + + const hookNameA = firstHelper.name + .replace('use', '') + .replace('Helper', ''); + const hookNameB = secondHelper.name + .replace('use', '') + .replace('Helper', ''); + + it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, () => { + function App(props) { + /* eslint-disable no-unused-vars */ + if (props.update) { + secondHelper(); + firstHelper(); + } else { + firstHelper(); + secondHelper(); + } + // This should not appear in the warning message because it occurs after the first mismatch + useRefHelper(); + return null; + /* eslint-enable no-unused-vars */ + } + let root = ReactTestRenderer.create(); + expect(() => { + try { + root.update(); + } catch (error) { + // Swapping certain types of hooks will cause runtime errors. + // This is okay as far as this test is concerned. + // We just want to verify that warnings are always logged. + } + }).toWarnDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + `1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameB)}\n` + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' + + ' in App (at **)', + ]); + + // further warnings for this component are silenced + try { + root.update(); + } catch (error) { + // Swapping certain types of hooks will cause runtime errors. + // This is okay as far as this test is concerned. + // We just want to verify that warnings are always logged. + } + }); - it('detects a bad hook order even if the component throws', () => { - const {useState, useReducer} = React; - function useCustomHook() { - useState(0); - } - function App(props) { - /* eslint-disable no-unused-vars */ - if (props.flip) { - useCustomHook(); - useReducer((s, a) => a, 0); - throw new Error('custom error'); - } else { - useReducer((s, a) => a, 0); - useCustomHook(); + it(`warns when more hooks (${(hookNameA, + hookNameB)}) are used during update than mount`, () => { + function App(props) { + /* eslint-disable no-unused-vars */ + if (props.update) { + firstHelper(); + secondHelper(); + } else { + firstHelper(); + } + return null; + /* eslint-enable no-unused-vars */ + } + let root = ReactTestRenderer.create(); + expect(() => { + try { + root.update(); + } catch (error) { + // Swapping certain types of hooks will cause runtime errors. + // This is okay as far as this test is concerned. + // We just want to verify that warnings are always logged. + } + }).toWarnDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + `1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameA)}\n` + + `2. undefined use${hookNameB}\n` + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' + + ' in App (at **)', + ]); + }); + }); + + // We don't include useContext or useDebugValue in this set, + // because they aren't added to the hooks list and so won't throw. + let hooksInList = [ + useCallbackHelper, + useEffectHelper, + useImperativeHandleHelper, + useLayoutEffectHelper, + useMemoHelper, + useReducerHelper, + useRefHelper, + useStateHelper, + ]; + + hooksInList.forEach((firstHelper, index) => { + const secondHelper = + index > 0 + ? hooksInList[index - 1] + : hooksInList[hooksInList.length - 1]; + + const hookNameA = firstHelper.name + .replace('use', '') + .replace('Helper', ''); + const hookNameB = secondHelper.name + .replace('use', '') + .replace('Helper', ''); + + it(`warns when fewer hooks (${(hookNameA, + hookNameB)}) are used during update than mount`, () => { + function App(props) { + /* eslint-disable no-unused-vars */ + if (props.update) { + firstHelper(); + } else { + firstHelper(); + secondHelper(); + } + return null; + /* eslint-enable no-unused-vars */ + } + let root = ReactTestRenderer.create(); + expect(() => { + root.update(); + }).toThrow('Rendered fewer hooks than expected.'); + }); + }); + + it( + 'warns on using differently ordered hooks ' + + '(useImperativeHandleHelper, useMemoHelper) on subsequent renders', + () => { + function App(props) { + /* eslint-disable no-unused-vars */ + if (props.update) { + useMemoHelper(); + useImperativeHandleHelper(); + } else { + useImperativeHandleHelper(); + useMemoHelper(); + } + // This should not appear in the warning message because it occurs after the first mismatch + useRefHelper(); + return null; + /* eslint-enable no-unused-vars */ + } + let root = ReactTestRenderer.create(); + expect(() => { + try { + root.update(); + } catch (error) { + // Swapping certain types of hooks will cause runtime errors. + // This is okay as far as this test is concerned. + // We just want to verify that warnings are always logged. + } + }).toWarnDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + `1. ${formatHookNamesToMatchErrorMessage( + 'ImperativeHandle', + 'Memo', + )}\n` + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' + + ' in App (at **)', + ]); + + // further warnings for this component are silenced + root.update(); + }, + ); + + it('detects a bad hook order even if the component throws', () => { + const {useState, useReducer} = React; + function useCustomHook() { + useState(0); } - return null; - /* eslint-enable no-unused-vars */ - } - let root = ReactTestRenderer.create(); - expect(() => { - expect(() => root.update()).toThrow('custom error'); - }).toWarnDev([ - 'Warning: React has detected a change in the order of Hooks called by App. ' + - 'This will lead to bugs and errors if not fixed. For more information, ' + - 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + - ' Previous render Next render\n' + - ' -------------------------------\n' + - '1. useReducer useState\n' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^', - ]); + function App(props) { + /* eslint-disable no-unused-vars */ + if (props.update) { + useCustomHook(); + useReducer((s, a) => a, 0); + throw new Error('custom error'); + } else { + useReducer((s, a) => a, 0); + useCustomHook(); + } + return null; + /* eslint-enable no-unused-vars */ + } + let root = ReactTestRenderer.create(); + expect(() => { + expect(() => root.update()).toThrow( + 'custom error', + ); + }).toWarnDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. useReducer useState\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ]); + }); }); // Regression test for #14674 diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index adca4cbc6de8..9a1c73a62f29 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -1739,8 +1739,20 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(() => { - expect(ReactNoop).toFlushAndYield(['A: 2, B: 3, C: 0']); - }).toThrow('Rendered more hooks than during the previous render'); + expect(() => { + expect(ReactNoop).toFlushAndYield(['A: 2, B: 3, C: 0']); + }).toThrow('Rendered more hooks than during the previous render'); + }).toWarnDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. useState useState\n' + + '2. useState useState\n' + + '3. undefined useState\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ]); // Uncomment if/when we support this again // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 0')]); @@ -1818,8 +1830,19 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.render(); expect(() => { - expect(ReactNoop).toFlushAndYield([]); - }).toThrow('Rendered more hooks than during the previous render'); + expect(() => { + expect(ReactNoop).toFlushAndYield([]); + }).toThrow('Rendered more hooks than during the previous render'); + }).toWarnDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. useEffect useEffect\n' + + '2. undefined useEffect\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ]); // Uncomment if/when we support this again // ReactNoop.flushPassiveEffects(); From c2fc6598b2bb61affeca3dd20e8d361e7ef9e810 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 26 Feb 2019 10:53:49 -0800 Subject: [PATCH 4/5] DEV mode hook mount/update check uses same logic as prod --- .../react-reconciler/src/ReactFiberHooks.js | 135 +++++++++--------- .../src/__tests__/ReactHooks-test.internal.js | 27 ++-- 2 files changed, 86 insertions(+), 76 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 86c0b8c37a2b..abb876882bbd 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -182,22 +182,24 @@ let currentHookNameInDev: ?HookType = null; // The list stores the order of hooks used during the initial render (mount). // Subsequent renders (updates) reference this list. let hookTypesDev: Array | null = null; -let hookTypesUpdateIndexDev: number = -1; -function mountHookTypeDev(hookName: HookType) { - if (__DEV__) { - if (hookTypesDev === null) { - hookTypesDev = [hookName]; - } else { - hookTypesDev.push(hookName); - } - } -} +// In DEV, this index is used to compare the order of hooks between update and mount. +// The index is null during mount (to distinguish between mount and update). +let hookTypesUpdateIndexDev: number | null = null; -function updateHookTypeDev(hookName: HookType) { +function checkHookTypesDev() { if (__DEV__) { - if (hookTypesDev !== null) { - if (hookTypesDev[++hookTypesUpdateIndexDev] !== hookName) { + const hookName = ((currentHookNameInDev: any): HookType); + + if (hookTypesUpdateIndexDev === null) { + if (hookTypesDev === null) { + hookTypesDev = [hookName]; + } else { + hookTypesDev.push(hookName); + } + } else if (hookTypesDev !== null) { + hookTypesUpdateIndexDev++; + if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) { warnOnHookMismatchInDev(hookName); } } @@ -217,10 +219,12 @@ function warnOnHookMismatchInDev(currentHookName: HookType) { const secondColumnStart = 30; - for (let i = 0; i <= hookTypesUpdateIndexDev; i++) { + for (let i = 0; i <= ((hookTypesUpdateIndexDev: any): number); i++) { const oldHookName = hookTypesDev[i]; const newHookName = - i === hookTypesUpdateIndexDev ? currentHookName : oldHookName; + i === ((hookTypesUpdateIndexDev: any): number) + ? currentHookName + : oldHookName; let row = `${i + 1}. ${oldHookName}`; @@ -319,7 +323,7 @@ export function renderWithHooks( current !== null ? ((current._debugHookTypes: any): Array) : null; - hookTypesUpdateIndexDev = -1; + hookTypesUpdateIndexDev = hookTypesDev === null ? null : -1; } // The following should have already been reset @@ -338,17 +342,17 @@ export function renderWithHooks( // Currently we will identify the update render as a mount because nextCurrentHook === null. // This is tricky because it's valid for certain types of components (e.g. React.lazy) + // This check is only accurate if at least one stateful hook is used. + // Non-stateful hooks (e.g. context) don't get added to memoizedState, + // so nextCurrentHook would be null during updates and mounts. + // We could use hookTypesDev to check this more reliably in DEV mode, + // but that might cause a potentially significant difference in behavior between DEV and prod. if (__DEV__) { ReactCurrentDispatcher.current = - hookTypesDev === null + nextCurrentHook === null ? HooksDispatcherOnMountInDEV : HooksDispatcherOnUpdateInDEV; } else { - // TODO This check isn't always accurate. - // Not all hooks are added to the Fiber's list (e.g. context) - // so using a non-null current hook might indicate "mount" when it's really an "update". - // We don't have a better data structure to check in production bundles though. - ReactCurrentDispatcher.current = nextCurrentHook === null ? HooksDispatcherOnMount @@ -371,7 +375,8 @@ export function renderWithHooks( componentUpdateQueue = null; if (__DEV__) { - hookTypesUpdateIndexDev = -1; + // Also validate hook order for cascading updates. + hookTypesUpdateIndexDev = hookTypesDev === null ? null : -1; } ReactCurrentDispatcher.current = __DEV__ @@ -417,7 +422,7 @@ export function renderWithHooks( if (__DEV__) { currentHookNameInDev = null; hookTypesDev = null; - hookTypesUpdateIndexDev = -1; + hookTypesUpdateIndexDev = null; } remainingExpirationTime = NoWork; @@ -469,7 +474,7 @@ export function resetHooks(): void { if (__DEV__) { hookTypesDev = null; - hookTypesUpdateIndexDev = -1; + hookTypesUpdateIndexDev = null; currentHookNameInDev = null; } @@ -1233,7 +1238,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountCallback(callback, deps); }, useContext( @@ -1241,7 +1246,7 @@ if (__DEV__) { observedBits: void | number | boolean, ): T { currentHookNameInDev = 'useContext'; - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return readContext(context, observedBits); }, useEffect( @@ -1249,7 +1254,7 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useEffect'; - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountEffect(create, deps); }, useImperativeHandle( @@ -1258,7 +1263,7 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useImperativeHandle'; - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1266,12 +1271,12 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useLayoutEffect'; - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1286,7 +1291,7 @@ if (__DEV__) { init?: I => S, ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1297,14 +1302,14 @@ if (__DEV__) { }, useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountRef(initialValue); }, useState( initialState: (() => S) | S, ): [S, Dispatch>] { currentHookNameInDev = 'useState'; - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1315,7 +1320,7 @@ if (__DEV__) { }, useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountDebugValue(value, formatterFn); }, }; @@ -1330,7 +1335,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateCallback(callback, deps); }, useContext( @@ -1338,7 +1343,7 @@ if (__DEV__) { observedBits: void | number | boolean, ): T { currentHookNameInDev = 'useContext'; - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return readContext(context, observedBits); }, useEffect( @@ -1346,7 +1351,7 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useEffect'; - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateEffect(create, deps); }, useImperativeHandle( @@ -1355,7 +1360,7 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useImperativeHandle'; - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1363,12 +1368,12 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useLayoutEffect'; - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1383,7 +1388,7 @@ if (__DEV__) { init?: I => S, ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1394,14 +1399,14 @@ if (__DEV__) { }, useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateRef(initialValue); }, useState( initialState: (() => S) | S, ): [S, Dispatch>] { currentHookNameInDev = 'useState'; - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1412,7 +1417,7 @@ if (__DEV__) { }, useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateDebugValue(value, formatterFn); }, }; @@ -1429,7 +1434,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; warnInvalidHookAccess(); - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountCallback(callback, deps); }, useContext( @@ -1438,7 +1443,7 @@ if (__DEV__) { ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return readContext(context, observedBits); }, useEffect( @@ -1447,7 +1452,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useEffect'; warnInvalidHookAccess(); - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountEffect(create, deps); }, useImperativeHandle( @@ -1457,7 +1462,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useImperativeHandle'; warnInvalidHookAccess(); - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1466,13 +1471,13 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useLayoutEffect'; warnInvalidHookAccess(); - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; warnInvalidHookAccess(); - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1488,7 +1493,7 @@ if (__DEV__) { ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; warnInvalidHookAccess(); - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1500,7 +1505,7 @@ if (__DEV__) { useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; warnInvalidHookAccess(); - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountRef(initialValue); }, useState( @@ -1508,7 +1513,7 @@ if (__DEV__) { ): [S, Dispatch>] { currentHookNameInDev = 'useState'; warnInvalidHookAccess(); - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1520,7 +1525,7 @@ if (__DEV__) { useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; warnInvalidHookAccess(); - mountHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return mountDebugValue(value, formatterFn); }, }; @@ -1537,7 +1542,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; warnInvalidHookAccess(); - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateCallback(callback, deps); }, useContext( @@ -1546,7 +1551,7 @@ if (__DEV__) { ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return readContext(context, observedBits); }, useEffect( @@ -1555,7 +1560,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useEffect'; warnInvalidHookAccess(); - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateEffect(create, deps); }, useImperativeHandle( @@ -1565,7 +1570,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useImperativeHandle'; warnInvalidHookAccess(); - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1574,13 +1579,13 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useLayoutEffect'; warnInvalidHookAccess(); - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; warnInvalidHookAccess(); - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1596,7 +1601,7 @@ if (__DEV__) { ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; warnInvalidHookAccess(); - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1608,7 +1613,7 @@ if (__DEV__) { useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; warnInvalidHookAccess(); - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateRef(initialValue); }, useState( @@ -1616,7 +1621,7 @@ if (__DEV__) { ): [S, Dispatch>] { currentHookNameInDev = 'useState'; warnInvalidHookAccess(); - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1628,7 +1633,7 @@ if (__DEV__) { useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; warnInvalidHookAccess(); - updateHookTypeDev(currentHookNameInDev); + checkHookTypesDev(); return updateDebugValue(value, formatterFn); }, }; diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index bcc3754bd19a..8a3e7d98a86f 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -984,8 +984,6 @@ describe('ReactHooks', () => { it('warns when calling hooks inside useReducer', () => { const {useReducer, useState, useRef} = React; - spyOnDev(console, 'error'); - function App() { const [value, dispatch] = useReducer((state, action) => { useRef(0); @@ -997,16 +995,23 @@ describe('ReactHooks', () => { useState(); return value; } - expect(() => { - ReactTestRenderer.create(); - }).toThrow('Rendered more hooks than during the previous render.'); - if (__DEV__) { - expect(console.error).toHaveBeenCalledTimes(4); - expect(console.error.calls.argsFor(0)[0]).toContain( - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', - ); - } + expect(() => { + expect(() => { + ReactTestRenderer.create(); + }).toThrow('Rendered more hooks than during the previous render.'); + }).toWarnDev([ + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. useReducer useReducer\n' + + '2. useState useRef\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ]); }); it("warns when calling hooks inside useState's initialize function", () => { From 38996818f71b8807e52bd382f8ba6f293cdb9198 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 26 Feb 2019 13:30:42 -0800 Subject: [PATCH 5/5] Added separate dispatcher for DEV-mode hooks update with no stateful hooks --- .../react-reconciler/src/ReactFiberHooks.js | 231 +++++++++++++----- 1 file changed, 169 insertions(+), 62 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index abb876882bbd..9244864b11c2 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -182,22 +182,25 @@ let currentHookNameInDev: ?HookType = null; // The list stores the order of hooks used during the initial render (mount). // Subsequent renders (updates) reference this list. let hookTypesDev: Array | null = null; +let hookTypesUpdateIndexDev: number = -1; -// In DEV, this index is used to compare the order of hooks between update and mount. -// The index is null during mount (to distinguish between mount and update). -let hookTypesUpdateIndexDev: number | null = null; +function mountHookTypesDev() { + if (__DEV__) { + const hookName = ((currentHookNameInDev: any): HookType); + + if (hookTypesDev === null) { + hookTypesDev = [hookName]; + } else { + hookTypesDev.push(hookName); + } + } +} -function checkHookTypesDev() { +function updateHookTypesDev() { if (__DEV__) { const hookName = ((currentHookNameInDev: any): HookType); - if (hookTypesUpdateIndexDev === null) { - if (hookTypesDev === null) { - hookTypesDev = [hookName]; - } else { - hookTypesDev.push(hookName); - } - } else if (hookTypesDev !== null) { + if (hookTypesDev !== null) { hookTypesUpdateIndexDev++; if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) { warnOnHookMismatchInDev(hookName); @@ -323,7 +326,7 @@ export function renderWithHooks( current !== null ? ((current._debugHookTypes: any): Array) : null; - hookTypesUpdateIndexDev = hookTypesDev === null ? null : -1; + hookTypesUpdateIndexDev = -1; } // The following should have already been reset @@ -342,16 +345,22 @@ export function renderWithHooks( // Currently we will identify the update render as a mount because nextCurrentHook === null. // This is tricky because it's valid for certain types of components (e.g. React.lazy) - // This check is only accurate if at least one stateful hook is used. + // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used. // Non-stateful hooks (e.g. context) don't get added to memoizedState, // so nextCurrentHook would be null during updates and mounts. - // We could use hookTypesDev to check this more reliably in DEV mode, - // but that might cause a potentially significant difference in behavior between DEV and prod. if (__DEV__) { - ReactCurrentDispatcher.current = - nextCurrentHook === null - ? HooksDispatcherOnMountInDEV - : HooksDispatcherOnUpdateInDEV; + if (nextCurrentHook !== null) { + ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV; + } else if (hookTypesDev !== null) { + // This dispatcher handles an edge case where a component is updating, + // but no stateful hooks have been used. + // We want to match the production code behavior (which will use HooksDispatcherOnMount), + // but with the extra DEV validation to ensure hooks ordering hasn't changed. + // This dispatcher does that. + ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV; + } else { + ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV; + } } else { ReactCurrentDispatcher.current = nextCurrentHook === null @@ -376,7 +385,7 @@ export function renderWithHooks( if (__DEV__) { // Also validate hook order for cascading updates. - hookTypesUpdateIndexDev = hookTypesDev === null ? null : -1; + hookTypesUpdateIndexDev = -1; } ReactCurrentDispatcher.current = __DEV__ @@ -422,7 +431,7 @@ export function renderWithHooks( if (__DEV__) { currentHookNameInDev = null; hookTypesDev = null; - hookTypesUpdateIndexDev = null; + hookTypesUpdateIndexDev = -1; } remainingExpirationTime = NoWork; @@ -474,7 +483,7 @@ export function resetHooks(): void { if (__DEV__) { hookTypesDev = null; - hookTypesUpdateIndexDev = null; + hookTypesUpdateIndexDev = -1; currentHookNameInDev = null; } @@ -1203,6 +1212,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { }; let HooksDispatcherOnMountInDEV: Dispatcher | null = null; +let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null; let HooksDispatcherOnUpdateInDEV: Dispatcher | null = null; let InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher | null = null; let InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher | null = null; @@ -1238,7 +1248,104 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; - checkHookTypesDev(); + mountHookTypesDev(); + return mountCallback(callback, deps); + }, + useContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + currentHookNameInDev = 'useContext'; + mountHookTypesDev(); + return readContext(context, observedBits); + }, + useEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useEffect'; + mountHookTypesDev(); + return mountEffect(create, deps); + }, + useImperativeHandle( + ref: {current: T | null} | ((inst: T | null) => mixed) | null | void, + create: () => T, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useImperativeHandle'; + mountHookTypesDev(); + return mountImperativeHandle(ref, create, deps); + }, + useLayoutEffect( + create: () => (() => void) | void, + deps: Array | void | null, + ): void { + currentHookNameInDev = 'useLayoutEffect'; + mountHookTypesDev(); + return mountLayoutEffect(create, deps); + }, + useMemo(create: () => T, deps: Array | void | null): T { + currentHookNameInDev = 'useMemo'; + mountHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountMemo(create, deps); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useReducer( + reducer: (S, A) => S, + initialArg: I, + init?: I => S, + ): [S, Dispatch] { + currentHookNameInDev = 'useReducer'; + mountHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountReducer(reducer, initialArg, init); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useRef(initialValue: T): {current: T} { + currentHookNameInDev = 'useRef'; + mountHookTypesDev(); + return mountRef(initialValue); + }, + useState( + initialState: (() => S) | S, + ): [S, Dispatch>] { + currentHookNameInDev = 'useState'; + mountHookTypesDev(); + const prevDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; + try { + return mountState(initialState); + } finally { + ReactCurrentDispatcher.current = prevDispatcher; + } + }, + useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { + currentHookNameInDev = 'useDebugValue'; + mountHookTypesDev(); + return mountDebugValue(value, formatterFn); + }, + }; + + HooksDispatcherOnMountWithHookTypesInDEV = { + readContext( + context: ReactContext, + observedBits: void | number | boolean, + ): T { + return readContext(context, observedBits); + }, + + useCallback(callback: T, deps: Array | void | null): T { + currentHookNameInDev = 'useCallback'; + updateHookTypesDev(); return mountCallback(callback, deps); }, useContext( @@ -1246,7 +1353,7 @@ if (__DEV__) { observedBits: void | number | boolean, ): T { currentHookNameInDev = 'useContext'; - checkHookTypesDev(); + updateHookTypesDev(); return readContext(context, observedBits); }, useEffect( @@ -1254,7 +1361,7 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useEffect'; - checkHookTypesDev(); + updateHookTypesDev(); return mountEffect(create, deps); }, useImperativeHandle( @@ -1263,7 +1370,7 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useImperativeHandle'; - checkHookTypesDev(); + updateHookTypesDev(); return mountImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1271,12 +1378,12 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useLayoutEffect'; - checkHookTypesDev(); + updateHookTypesDev(); return mountLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; - checkHookTypesDev(); + updateHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1291,7 +1398,7 @@ if (__DEV__) { init?: I => S, ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; - checkHookTypesDev(); + updateHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1302,14 +1409,14 @@ if (__DEV__) { }, useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; - checkHookTypesDev(); + updateHookTypesDev(); return mountRef(initialValue); }, useState( initialState: (() => S) | S, ): [S, Dispatch>] { currentHookNameInDev = 'useState'; - checkHookTypesDev(); + updateHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1320,7 +1427,7 @@ if (__DEV__) { }, useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; - checkHookTypesDev(); + updateHookTypesDev(); return mountDebugValue(value, formatterFn); }, }; @@ -1335,7 +1442,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; - checkHookTypesDev(); + updateHookTypesDev(); return updateCallback(callback, deps); }, useContext( @@ -1343,7 +1450,7 @@ if (__DEV__) { observedBits: void | number | boolean, ): T { currentHookNameInDev = 'useContext'; - checkHookTypesDev(); + updateHookTypesDev(); return readContext(context, observedBits); }, useEffect( @@ -1351,7 +1458,7 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useEffect'; - checkHookTypesDev(); + updateHookTypesDev(); return updateEffect(create, deps); }, useImperativeHandle( @@ -1360,7 +1467,7 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useImperativeHandle'; - checkHookTypesDev(); + updateHookTypesDev(); return updateImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1368,12 +1475,12 @@ if (__DEV__) { deps: Array | void | null, ): void { currentHookNameInDev = 'useLayoutEffect'; - checkHookTypesDev(); + updateHookTypesDev(); return updateLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; - checkHookTypesDev(); + updateHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1388,7 +1495,7 @@ if (__DEV__) { init?: I => S, ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; - checkHookTypesDev(); + updateHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1399,14 +1506,14 @@ if (__DEV__) { }, useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; - checkHookTypesDev(); + updateHookTypesDev(); return updateRef(initialValue); }, useState( initialState: (() => S) | S, ): [S, Dispatch>] { currentHookNameInDev = 'useState'; - checkHookTypesDev(); + updateHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1417,7 +1524,7 @@ if (__DEV__) { }, useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; - checkHookTypesDev(); + updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, }; @@ -1434,7 +1541,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; warnInvalidHookAccess(); - checkHookTypesDev(); + mountHookTypesDev(); return mountCallback(callback, deps); }, useContext( @@ -1443,7 +1550,7 @@ if (__DEV__) { ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); - checkHookTypesDev(); + mountHookTypesDev(); return readContext(context, observedBits); }, useEffect( @@ -1452,7 +1559,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useEffect'; warnInvalidHookAccess(); - checkHookTypesDev(); + mountHookTypesDev(); return mountEffect(create, deps); }, useImperativeHandle( @@ -1462,7 +1569,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useImperativeHandle'; warnInvalidHookAccess(); - checkHookTypesDev(); + mountHookTypesDev(); return mountImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1471,13 +1578,13 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useLayoutEffect'; warnInvalidHookAccess(); - checkHookTypesDev(); + mountHookTypesDev(); return mountLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; warnInvalidHookAccess(); - checkHookTypesDev(); + mountHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1493,7 +1600,7 @@ if (__DEV__) { ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; warnInvalidHookAccess(); - checkHookTypesDev(); + mountHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1505,7 +1612,7 @@ if (__DEV__) { useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; warnInvalidHookAccess(); - checkHookTypesDev(); + mountHookTypesDev(); return mountRef(initialValue); }, useState( @@ -1513,7 +1620,7 @@ if (__DEV__) { ): [S, Dispatch>] { currentHookNameInDev = 'useState'; warnInvalidHookAccess(); - checkHookTypesDev(); + mountHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { @@ -1525,7 +1632,7 @@ if (__DEV__) { useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; warnInvalidHookAccess(); - checkHookTypesDev(); + mountHookTypesDev(); return mountDebugValue(value, formatterFn); }, }; @@ -1542,7 +1649,7 @@ if (__DEV__) { useCallback(callback: T, deps: Array | void | null): T { currentHookNameInDev = 'useCallback'; warnInvalidHookAccess(); - checkHookTypesDev(); + updateHookTypesDev(); return updateCallback(callback, deps); }, useContext( @@ -1551,7 +1658,7 @@ if (__DEV__) { ): T { currentHookNameInDev = 'useContext'; warnInvalidHookAccess(); - checkHookTypesDev(); + updateHookTypesDev(); return readContext(context, observedBits); }, useEffect( @@ -1560,7 +1667,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useEffect'; warnInvalidHookAccess(); - checkHookTypesDev(); + updateHookTypesDev(); return updateEffect(create, deps); }, useImperativeHandle( @@ -1570,7 +1677,7 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useImperativeHandle'; warnInvalidHookAccess(); - checkHookTypesDev(); + updateHookTypesDev(); return updateImperativeHandle(ref, create, deps); }, useLayoutEffect( @@ -1579,13 +1686,13 @@ if (__DEV__) { ): void { currentHookNameInDev = 'useLayoutEffect'; warnInvalidHookAccess(); - checkHookTypesDev(); + updateHookTypesDev(); return updateLayoutEffect(create, deps); }, useMemo(create: () => T, deps: Array | void | null): T { currentHookNameInDev = 'useMemo'; warnInvalidHookAccess(); - checkHookTypesDev(); + updateHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1601,7 +1708,7 @@ if (__DEV__) { ): [S, Dispatch] { currentHookNameInDev = 'useReducer'; warnInvalidHookAccess(); - checkHookTypesDev(); + updateHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1613,7 +1720,7 @@ if (__DEV__) { useRef(initialValue: T): {current: T} { currentHookNameInDev = 'useRef'; warnInvalidHookAccess(); - checkHookTypesDev(); + updateHookTypesDev(); return updateRef(initialValue); }, useState( @@ -1621,7 +1728,7 @@ if (__DEV__) { ): [S, Dispatch>] { currentHookNameInDev = 'useState'; warnInvalidHookAccess(); - checkHookTypesDev(); + updateHookTypesDev(); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { @@ -1633,7 +1740,7 @@ if (__DEV__) { useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { currentHookNameInDev = 'useDebugValue'; warnInvalidHookAccess(); - checkHookTypesDev(); + updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, };