From 97fd3e7064b162f05b1bac3962ed10c6559c346c Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 6 Feb 2024 17:53:08 +0100 Subject: [PATCH] Ensure useState and useReducer initializer functions are double invoked in StrictMode (#28248) --- .../react-reconciler/src/ReactFiberHooks.js | 14 ++++++- .../src/__tests__/ReactStrictMode-test.js | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 8e50d3e6d6678..345e85d3c427b 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1154,6 +1154,11 @@ function mountReducer( let initialState; if (init !== undefined) { initialState = init(initialArg); + if (shouldDoubleInvokeUserFnsInHooksDEV) { + setIsStrictModeForDevtools(true); + init(initialArg); + setIsStrictModeForDevtools(false); + } } else { initialState = ((initialArg: any): S); } @@ -1745,8 +1750,15 @@ function forceStoreRerender(fiber: Fiber) { function mountStateImpl(initialState: (() => S) | S): Hook { const hook = mountWorkInProgressHook(); if (typeof initialState === 'function') { + const initialStateInitializer = initialState; // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types - initialState = initialState(); + initialState = initialStateInitializer(); + if (shouldDoubleInvokeUserFnsInHooksDEV) { + setIsStrictModeForDevtools(true); + // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types + initialStateInitializer(); + setIsStrictModeForDevtools(false); + } } hook.memoizedState = hook.baseState = initialState; const queue: UpdateQueue> = { diff --git a/packages/react/src/__tests__/ReactStrictMode-test.js b/packages/react/src/__tests__/ReactStrictMode-test.js index c0a4d59f3d135..28dd94ad06ca2 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.js @@ -202,6 +202,46 @@ describe('ReactStrictMode', () => { expect(instance.state.count).toBe(2); }); + // @gate debugRenderPhaseSideEffectsForStrictMode + it('double invokes useState and useReducer initializers functions', async () => { + const log = []; + + function App() { + React.useState(() => { + log.push('Compute initial state count: 1'); + return 1; + }); + React.useReducer( + s => s, + 2, + s => { + log.push('Compute initial reducer count: 2'); + return s; + }, + ); + + return 3; + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + + + , + ); + }); + expect(container.textContent).toBe('3'); + + expect(log).toEqual([ + 'Compute initial state count: 1', + 'Compute initial state count: 1', + 'Compute initial reducer count: 2', + 'Compute initial reducer count: 2', + ]); + }); + it('should invoke only precommit lifecycle methods twice in DEV legacy roots', async () => { const {StrictMode} = React;