From 6596c5cd0f8228236c27c90582d773f3c918531e Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 5 Feb 2024 19:24:09 +0100 Subject: [PATCH 1/5] Ensure useState and useReducer initializer functions are double invoked in StrictMode --- .../react-reconciler/src/ReactFiberHooks.js | 9 ++++- .../src/__tests__/ReactStrictMode-test.js | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 2ace73aa69ad6..15e2b014528eb 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1158,6 +1158,9 @@ function mountReducer( let initialState; if (init !== undefined) { initialState = init(initialArg); + if (shouldDoubleInvokeUserFnsInHooksDEV) { + init(initialArg); + } } else { initialState = ((initialArg: any): S); } @@ -1749,8 +1752,12 @@ 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) { + initialStateInitializer(); + } } 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..59728acc9eda7 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 count state: 1'); + return 1; + }); + React.useReducer( + s => s, + 2, + s => { + log.push('Compute initial reducer state: 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 count state: 1', + 'Compute initial count state: 1', + 'Compute initial reducer state: 2', + 'Compute initial reducer state: 2', + ]); + }); + it('should invoke only precommit lifecycle methods twice in DEV legacy roots', async () => { const {StrictMode} = React; From c40cd298395d40cfedc8d6947f9febd6b941a82b Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 5 Feb 2024 19:34:01 +0100 Subject: [PATCH 2/5] Fix Flow --- packages/react-reconciler/src/ReactFiberHooks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 15e2b014528eb..df81c3fab1a7e 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1756,6 +1756,7 @@ function mountStateImpl(initialState: (() => S) | S): Hook { // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types initialState = initialStateInitializer(); if (shouldDoubleInvokeUserFnsInHooksDEV) { + // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types initialStateInitializer(); } } From 69abbc56f693239cf63f92d26d334906084f7cf0 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 5 Feb 2024 19:36:53 +0100 Subject: [PATCH 3/5] buff up wording --- packages/react/src/__tests__/ReactStrictMode-test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react/src/__tests__/ReactStrictMode-test.js b/packages/react/src/__tests__/ReactStrictMode-test.js index 59728acc9eda7..28dd94ad06ca2 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.js @@ -208,14 +208,14 @@ describe('ReactStrictMode', () => { function App() { React.useState(() => { - log.push('Compute initial count state: 1'); + log.push('Compute initial state count: 1'); return 1; }); React.useReducer( s => s, 2, s => { - log.push('Compute initial reducer state: 2'); + log.push('Compute initial reducer count: 2'); return s; }, ); @@ -235,10 +235,10 @@ describe('ReactStrictMode', () => { expect(container.textContent).toBe('3'); expect(log).toEqual([ - 'Compute initial count state: 1', - 'Compute initial count state: 1', - 'Compute initial reducer state: 2', - 'Compute initial reducer state: 2', + 'Compute initial state count: 1', + 'Compute initial state count: 1', + 'Compute initial reducer count: 2', + 'Compute initial reducer count: 2', ]); }); From f6d0c63c857d220f6b3221eb81ab8c56a7cda3b0 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 5 Feb 2024 19:59:18 +0100 Subject: [PATCH 4/5] Ensure second invocation is dimmed --- packages/react-reconciler/src/ReactFiberHooks.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index df81c3fab1a7e..6557e8dbcbce0 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1159,7 +1159,12 @@ function mountReducer( if (init !== undefined) { initialState = init(initialArg); if (shouldDoubleInvokeUserFnsInHooksDEV) { - init(initialArg); + setIsStrictModeForDevtools(true); + try { + init(initialArg); + } finally { + setIsStrictModeForDevtools(false); + } } } else { initialState = ((initialArg: any): S); @@ -1756,8 +1761,13 @@ function mountStateImpl(initialState: (() => S) | S): Hook { // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types initialState = initialStateInitializer(); if (shouldDoubleInvokeUserFnsInHooksDEV) { - // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types - initialStateInitializer(); + setIsStrictModeForDevtools(true); + try { + // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types + initialStateInitializer(); + } finally { + setIsStrictModeForDevtools(false); + } } } hook.memoizedState = hook.baseState = initialState; From 3b83783e7296184c9f73522803502d6758a2fdf7 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 6 Feb 2024 17:39:41 +0100 Subject: [PATCH 5/5] No try-finally for consistency --- packages/react-reconciler/src/ReactFiberHooks.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 6557e8dbcbce0..af3321921ea12 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1160,11 +1160,8 @@ function mountReducer( initialState = init(initialArg); if (shouldDoubleInvokeUserFnsInHooksDEV) { setIsStrictModeForDevtools(true); - try { - init(initialArg); - } finally { - setIsStrictModeForDevtools(false); - } + init(initialArg); + setIsStrictModeForDevtools(false); } } else { initialState = ((initialArg: any): S); @@ -1762,12 +1759,9 @@ function mountStateImpl(initialState: (() => S) | S): Hook { initialState = initialStateInitializer(); if (shouldDoubleInvokeUserFnsInHooksDEV) { setIsStrictModeForDevtools(true); - try { - // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types - initialStateInitializer(); - } finally { - setIsStrictModeForDevtools(false); - } + // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types + initialStateInitializer(); + setIsStrictModeForDevtools(false); } } hook.memoizedState = hook.baseState = initialState;