From c2f1a2c818510e7dd2764307c61f73f69d064791 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Thu, 14 Mar 2024 11:05:57 +0100 Subject: [PATCH 1/3] Failing test for null dispatch returned from `useFormState` in `StrictMode` --- .../src/__tests__/ReactDOMForm-test.js | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 977439b099971..180d913b6e825 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -1295,4 +1295,48 @@ describe('ReactDOMForm', () => { assertLog(['B']); expect(container.textContent).toBe('B'); }); + + + // @gate enableFormActions + // @gate enableAsyncActions + test('useFormState works in StrictMode', async () => { + let actionCounter = 0; + async function action(state, type) { + actionCounter++; + + Scheduler.log(`Async action started [${actionCounter}]`); + await getText(`Wait [${actionCounter}]`); + + switch (type) { + case 'increment': + return state + 1; + case 'decrement': + return state - 1; + default: + return state; + } + } + + let dispatch; + function App() { + const [state, _dispatch, isPending] = useFormState(action, 0); + dispatch = _dispatch; + const pending = isPending ? 'Pending ' : ''; + return ; + } + + const root = ReactDOMClient.createRoot(container); + await act(() => root.render()); + assertLog(['0']); + expect(container.textContent).toBe('0'); + + await act(() => dispatch('increment')); + assertLog(['Async action started [1]', 'Pending 0']); + expect(container.textContent).toBe('Pending 0'); + + + await act(() => resolveText('Wait [1]')); + assertLog(['1']); + expect(container.textContent).toBe('1'); + }); }); From 9b3f1f3746f4e072c286d7b077178a9211ee2e5d Mon Sep 17 00:00:00 2001 From: eps1lon Date: Thu, 14 Mar 2024 11:17:30 +0100 Subject: [PATCH 2/3] Ensure dispatch in `useFormState` works in `StrictMode` --- packages/react-dom/src/__tests__/ReactDOMForm-test.js | 10 +++++++--- packages/react-reconciler/src/ReactFiberHooks.js | 6 ++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 180d913b6e825..ea97b293ff707 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -1296,7 +1296,6 @@ describe('ReactDOMForm', () => { expect(container.textContent).toBe('B'); }); - // @gate enableFormActions // @gate enableAsyncActions test('useFormState works in StrictMode', async () => { @@ -1326,7 +1325,13 @@ describe('ReactDOMForm', () => { } const root = ReactDOMClient.createRoot(container); - await act(() => root.render()); + await act(() => + root.render( + + + , + ), + ); assertLog(['0']); expect(container.textContent).toBe('0'); @@ -1334,7 +1339,6 @@ describe('ReactDOMForm', () => { assertLog(['Async action started [1]', 'Pending 0']); expect(container.textContent).toBe('Pending 0'); - await act(() => resolveText('Wait [1]')); assertLog(['1']); expect(container.textContent).toBe('1'); diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 75acffc1b9a11..4f65da389c2a6 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -2259,6 +2259,9 @@ function rerenderFormState( ); } + // For mount, pending is always false. + const [isPending] = rerenderState(false); + // This is a mount. No updates to process. const state: Awaited = stateHook.memoizedState; @@ -2269,8 +2272,7 @@ function rerenderFormState( // This may have changed during the rerender. actionQueueHook.memoizedState = action; - // For mount, pending is always false. - return [state, dispatch, false]; + return [state, dispatch, isPending]; } function pushEffect( From e32f38a05b1861af1be67ce223dafbf5f83e4799 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Tue, 19 Mar 2024 19:32:05 +0100 Subject: [PATCH 3/3] Just advance the hook list Co-Authored-By: Andrew Clark --- packages/react-reconciler/src/ReactFiberHooks.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 4f65da389c2a6..46a21cf364b06 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -2259,8 +2259,7 @@ function rerenderFormState( ); } - // For mount, pending is always false. - const [isPending] = rerenderState(false); + updateWorkInProgressHook(); // State // This is a mount. No updates to process. const state: Awaited = stateHook.memoizedState; @@ -2272,7 +2271,8 @@ function rerenderFormState( // This may have changed during the rerender. actionQueueHook.memoizedState = action; - return [state, dispatch, isPending]; + // For mount, pending is always false. + return [state, dispatch, false]; } function pushEffect(