From a1414e8949915c006bba411fa2949b304dd044ae Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 21 Jan 2019 15:35:47 +0000 Subject: [PATCH] Double-render function components with Hooks in DEV in StrictMode (#14643) * Double-render functions in strict mode * Double-invoke first function component render too * Mark TestRendererAsync test as internal and revert changes to it TestRenderer is built with strict mode doublerender off. We could change that but I'm not sure we want to. So I'll just flip the flag off for this test. * Only double-invoke components using Hooks * Revert unintentional change --- .../src/ReactFiberBeginWork.js | 53 +++++ .../src/__tests__/ReactHooks-test.internal.js | 220 ++++++++++++++++++ 2 files changed, 273 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index a00b795b16dd4..8328b694f0724 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -250,6 +250,23 @@ function updateForwardRef( ref, renderExpirationTime, ); + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Only double-render components with Hooks + if (workInProgress.memoizedState !== null) { + renderWithHooks( + current, + workInProgress, + render, + nextProps, + ref, + renderExpirationTime, + ); + } + } setCurrentPhase(null); } else { nextChildren = renderWithHooks( @@ -543,6 +560,23 @@ function updateFunctionComponent( context, renderExpirationTime, ); + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Only double-render components with Hooks + if (workInProgress.memoizedState !== null) { + renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderExpirationTime, + ); + } + } setCurrentPhase(null); } else { nextChildren = renderWithHooks( @@ -1207,6 +1241,25 @@ function mountIndeterminateComponent( } else { // Proceed under the assumption that this is a function component workInProgress.tag = FunctionComponent; + if (__DEV__) { + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Only double-render components with Hooks + if (workInProgress.memoizedState !== null) { + renderWithHooks( + null, + workInProgress, + Component, + props, + context, + renderExpirationTime, + ); + } + } + } reconcileChildren(null, workInProgress, value, renderExpirationTime); if (__DEV__) { validateFunctionComponentInDev(workInProgress, Component); diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 19fa96e8cdc19..d13755ebddb95 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -660,4 +660,224 @@ describe('ReactHooks', () => { 'Hooks can only be called inside the body of a function component', ); }); + + it('double-invokes components with Hooks in Strict Mode', () => { + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = true; + + const {useState, StrictMode} = React; + let renderCount = 0; + + function NoHooks() { + renderCount++; + return
; + } + + function HasHooks() { + useState(0); + renderCount++; + return
; + } + + const FwdRef = React.forwardRef((props, ref) => { + renderCount++; + return
; + }); + + const FwdRefHasHooks = React.forwardRef((props, ref) => { + useState(0); + renderCount++; + return
; + }); + + const Memo = React.memo(props => { + renderCount++; + return
; + }); + + const MemoHasHooks = React.memo(props => { + useState(0); + renderCount++; + return
; + }); + + function Factory() { + return { + state: {}, + render() { + renderCount++; + return
; + }, + }; + } + + let renderer = ReactTestRenderer.create(null); + + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(1); + + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(1); + + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(1); + + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class + + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks + + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks + + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update(); + expect(renderCount).toBe(1); + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks + renderCount = 0; + renderer.update( + + + , + ); + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks + }); + + it('double-invokes useMemo in DEV StrictMode despite []', () => { + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = true; + const {useMemo, StrictMode} = React; + + let useMemoCount = 0; + function BadUseMemo() { + useMemo(() => { + useMemoCount++; + }, []); + return
; + } + + useMemoCount = 0; + ReactTestRenderer.create( + + + , + ); + expect(useMemoCount).toBe(__DEV__ ? 2 : 1); // Has Hooks + }); });