diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js index a616ff395f..37315929e4 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js @@ -24,7 +24,7 @@ let useContext; let useCallback; let useMemo; let useRef; -let useAPI; +let useImperativeMethods; let useMutationEffect; let useLayoutEffect; let forwardRef; @@ -49,7 +49,7 @@ function initModules() { useCallback = React.useCallback; useMemo = React.useMemo; useRef = React.useRef; - useAPI = React.useAPI; + useImperativeMethods = React.useImperativeMethods; useMutationEffect = React.useMutationEffect; useLayoutEffect = React.useLayoutEffect; forwardRef = React.forwardRef; @@ -518,10 +518,10 @@ describe('ReactDOMServerHooks', () => { }); }); - describe('useAPI', () => { + describe('useImperativeMethods', () => { it('should not be invoked on the server', async () => { function Counter(props, ref) { - useAPI(ref, () => { + useImperativeMethods(ref, () => { throw new Error('should not be invoked'); }); return ; diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index 1bebbc7444..b859e6dfca 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -365,8 +365,8 @@ export const Dispatcher = { useState, useMutationEffect, useLayoutEffect, - // useAPI is not run in the server environment - useAPI: noop, + // useImperativeMethods is not run in the server environment + useImperativeMethods: noop, // Callbacks are not run in the server environment. useCallback: noop, // Effects are not run in the server environment. diff --git a/packages/react-reconciler/src/ReactFiberDispatcher.js b/packages/react-reconciler/src/ReactFiberDispatcher.js index 10c0dee649..b5f27df334 100644 --- a/packages/react-reconciler/src/ReactFiberDispatcher.js +++ b/packages/react-reconciler/src/ReactFiberDispatcher.js @@ -9,10 +9,10 @@ import {readContext} from './ReactFiberNewContext'; import { - useAPI, useCallback, useContext, useEffect, + useImperativeMethods, useLayoutEffect, useMemo, useMutationEffect, @@ -23,10 +23,10 @@ import { export const Dispatcher = { readContext, - useAPI, useCallback, useContext, useEffect, + useImperativeMethods, useLayoutEffect, useMemo, useMutationEffect, diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index eef5c602b0..35ad699d20 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -179,9 +179,7 @@ export function finishHooks( renderedWork.memoizedState = firstWorkInProgressHook; renderedWork.expirationTime = remainingExpirationTime; - if (componentUpdateQueue !== null) { - renderedWork.updateQueue = (componentUpdateQueue: any); - } + renderedWork.updateQueue = (componentUpdateQueue: any); const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null; @@ -576,26 +574,27 @@ function useEffectImpl(fiberEffectTag, hookEffectTag, create, inputs): void { currentlyRenderingFiber = resolveCurrentlyRenderingFiber(); workInProgressHook = createWorkInProgressHook(); - let nextEffect; let nextInputs = inputs !== undefined && inputs !== null ? inputs : [create]; + let destroy = null; if (currentHook !== null) { const prevEffect = currentHook.memoizedState; - const prevInputs = prevEffect.inputs; - nextEffect = pushEffect( - inputsAreEqual(nextInputs, prevInputs) ? NoHookEffect : hookEffectTag, - create, - prevEffect.destroy, - nextInputs, - ); - } else { - nextEffect = pushEffect(hookEffectTag, create, null, nextInputs); + destroy = prevEffect.destroy; + if (inputsAreEqual(nextInputs, prevEffect.inputs)) { + pushEffect(NoHookEffect, create, destroy, nextInputs); + return; + } } - workInProgressHook.memoizedState = nextEffect; currentlyRenderingFiber.effectTag |= fiberEffectTag; + workInProgressHook.memoizedState = pushEffect( + hookEffectTag, + create, + destroy, + nextInputs, + ); } -export function useAPI( +export function useImperativeMethods( ref: {current: T | null} | ((inst: T | null) => mixed) | null | void, create: () => T, inputs: Array | void | null, diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 9ee5ef633c..c6844256d6 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -24,7 +24,7 @@ let useLayoutEffect; let useCallback; let useMemo; let useRef; -let useAPI; +let useImperativeMethods; let forwardRef; let flushPassiveEffects; let memo; @@ -70,7 +70,7 @@ describe('ReactHooks', () => { useCallback = React.useCallback; useMemo = React.useMemo; useRef = React.useRef; - useAPI = React.useAPI; + useImperativeMethods = React.useImperativeMethods; forwardRef = React.forwardRef; memo = React.memo; }); @@ -87,7 +87,7 @@ describe('ReactHooks', () => { it('resumes after an interruption', () => { function Counter(props, ref) { const [count, updateCount] = useState(0); - useAPI(ref, () => ({updateCount})); + useImperativeMethods(ref, () => ({updateCount})); return ; } Counter = forwardRef(Counter); @@ -171,7 +171,7 @@ describe('ReactHooks', () => { it('simple mount and update', () => { function Counter(props, ref) { const [count, updateCount] = useState(0); - useAPI(ref, () => ({updateCount})); + useImperativeMethods(ref, () => ({updateCount})); return ; } Counter = forwardRef(Counter); @@ -195,7 +195,7 @@ describe('ReactHooks', () => { ReactNoop.yield('getInitialState'); return props.initialState; }); - useAPI(ref, () => ({updateCount})); + useImperativeMethods(ref, () => ({updateCount})); return ; } Counter = forwardRef(Counter); @@ -213,7 +213,7 @@ describe('ReactHooks', () => { function Counter(props, ref) { const [count, updateCount] = useState(0); const [label, updateLabel] = useState('Count'); - useAPI(ref, () => ({updateCount, updateLabel})); + useImperativeMethods(ref, () => ({updateCount, updateLabel})); return ; } Counter = forwardRef(Counter); @@ -232,7 +232,7 @@ describe('ReactHooks', () => { it('callbacks', () => { function Counter(props, ref) { const [count, updateCount] = useState(0); - useAPI(ref, () => ({updateCount})); + useImperativeMethods(ref, () => ({updateCount})); return ; } Counter = forwardRef(Counter); @@ -264,7 +264,7 @@ describe('ReactHooks', () => { it('does not fire callbacks more than once when rebasing', () => { function Counter(props, ref) { const [count, updateCount] = useState(0); - useAPI(ref, () => ({updateCount})); + useImperativeMethods(ref, () => ({updateCount})); return ; } Counter = forwardRef(Counter); @@ -507,7 +507,7 @@ describe('ReactHooks', () => { function Counter({row: newRow}, ref) { let [reducer, setReducer] = useState(() => reducerA); let [count, dispatch] = useReducer(reducer, 0); - useAPI(ref, () => ({dispatch})); + useImperativeMethods(ref, () => ({dispatch})); if (count < 20) { dispatch('increment'); // Swap reducers each time we increment @@ -568,7 +568,7 @@ describe('ReactHooks', () => { function Counter(props, ref) { const [count, dispatch] = useReducer(reducer, 0); - useAPI(ref, () => ({dispatch})); + useImperativeMethods(ref, () => ({dispatch})); return ; } Counter = forwardRef(Counter); @@ -609,7 +609,7 @@ describe('ReactHooks', () => { function Counter(props, ref) { const [count, dispatch] = useReducer(reducer, 0, initialAction); - useAPI(ref, () => ({dispatch})); + useImperativeMethods(ref, () => ({dispatch})); return ; } Counter = forwardRef(Counter); @@ -910,7 +910,6 @@ describe('ReactHooks', () => { expect(ReactNoop.flush()).toEqual(['Will set count to 1', 'Count: 2']); expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]); - flushPassiveEffects(); expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); expect(onWorkCanceled).toHaveBeenCalledTimes(0); }); @@ -1019,6 +1018,33 @@ describe('ReactHooks', () => { expect(ReactNoop.getChildren()).toEqual([]); }); + it('unmounts on deletion after skipped effect', () => { + function Counter(props) { + useEffect(() => { + ReactNoop.yield(`Did create [${props.count}]`); + return () => { + ReactNoop.yield(`Did destroy [${props.count}]`); + }; + }, []); + return ; + } + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + flushPassiveEffects(); + expect(ReactNoop.clearYields()).toEqual(['Did create [0]']); + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + flushPassiveEffects(); + expect(ReactNoop.clearYields()).toEqual(null); + + ReactNoop.render(null); + expect(ReactNoop.flush()).toEqual(['Did destroy [0]']); + expect(ReactNoop.getChildren()).toEqual([]); + }); + it('skips effect if constructor has not changed', () => { function effect() { ReactNoop.yield(`Did mount`); diff --git a/packages/react/src/React.js b/packages/react/src/React.js index b2bbf2e8d2..04f3592ced 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -29,10 +29,10 @@ import {lazy} from './ReactLazy'; import forwardRef from './forwardRef'; import memo from './memo'; import { - useAPI, useCallback, useContext, useEffect, + useImperativeMethods, useLayoutEffect, useMemo, useMutationEffect, @@ -89,10 +89,10 @@ if (enableStableConcurrentModeAPIs) { } if (enableHooks) { - React.useAPI = useAPI; React.useCallback = useCallback; React.useContext = useContext; React.useEffect = useEffect; + React.useImperativeMethods = useImperativeMethods; React.useLayoutEffect = useLayoutEffect; React.useMemo = useMemo; React.useMutationEffect = useMutationEffect; diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 1cc8b79ac5..0e8f46dddb 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -110,11 +110,11 @@ export function useMemo( return dispatcher.useMemo(create, inputs); } -export function useAPI( +export function useImperativeMethods( ref: {current: T | null} | ((inst: T | null) => mixed) | null | void, create: () => T, inputs: Array | void | null, ): void { const dispatcher = resolveDispatcher(); - return dispatcher.useAPI(ref, create, inputs); + return dispatcher.useImperativeMethods(ref, create, inputs); }