Skip to content

Commit ba3e43b

Browse files
committed
Scaffolding for useFormState
This exposes, but does not yet implement, a new experimental API called useFormState. It's gated behind the enableAsyncActions flag. useFormState has a similar signature to useReducer, except instead of a reducer it accepts an (async) action function. React will wait until the promise resolves before updating the state: async function action(prevState, payload) { // .. } const [state, dispatch] = useFormState(action, initialState) When used in combination with Server Actions, it will also support progressive enhancement — a form that is submitted before it has hydrated will have its state transferred to the next page. However, like the other action-related hooks, it works with fully client-driven actions, too.
1 parent 856dc5e commit ba3e43b

File tree

15 files changed

+205
-4
lines changed

15 files changed

+205
-4
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ module.exports = {
456456
$ReadOnlyArray: 'readonly',
457457
$ArrayBufferView: 'readonly',
458458
$Shape: 'readonly',
459+
ReturnType: 'readonly',
459460
AnimationFrameID: 'readonly',
460461
// For Flow type annotation. Only `BigInt` is valid at runtime.
461462
bigint: 'readonly',

packages/react-dom-bindings/src/shared/ReactDOMFormActions.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,17 @@ export function useFormStatus(): FormStatus {
7474
return dispatcher.useHostTransitionStatus();
7575
}
7676
}
77+
78+
export function useFormState<S, P>(
79+
action: (S, P) => S,
80+
initialState: S,
81+
url?: string,
82+
): [S, (P) => void] {
83+
if (!(enableFormActions && enableAsyncActions)) {
84+
throw new Error('Not implemented.');
85+
} else {
86+
const dispatcher = resolveDispatcher();
87+
// $FlowFixMe[not-a-function] This is unstable, thus optional
88+
return dispatcher.useFormState(action, initialState, url);
89+
}
90+
}

packages/react-dom/index.classic.fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export {
3232
unstable_renderSubtreeIntoContainer,
3333
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
3434
useFormStatus as experimental_useFormStatus,
35+
useFormState as experimental_useFormState,
3536
prefetchDNS,
3637
preconnect,
3738
preload,

packages/react-dom/index.experimental.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export {
2121
unstable_renderSubtreeIntoContainer,
2222
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
2323
useFormStatus as experimental_useFormStatus,
24+
useFormState as experimental_useFormState,
2425
prefetchDNS,
2526
preconnect,
2627
preload,

packages/react-dom/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export {
2424
unstable_renderSubtreeIntoContainer,
2525
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
2626
useFormStatus as experimental_useFormStatus,
27+
useFormState as experimental_useFormState,
2728
prefetchDNS,
2829
preconnect,
2930
preload,

packages/react-dom/index.modern.fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export {
1717
unstable_createEventHandle,
1818
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
1919
useFormStatus as experimental_useFormStatus,
20+
useFormState as experimental_useFormState,
2021
prefetchDNS,
2122
preconnect,
2223
preload,

packages/react-dom/server-rendering-stub.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ export {
2323
preload,
2424
preinit,
2525
experimental_useFormStatus,
26+
experimental_useFormState,
2627
unstable_batchedUpdates,
2728
} from './src/server/ReactDOMServerRenderingStub';

packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ let ReactDOMServer;
2323
let ReactDOMClient;
2424
let useFormStatus;
2525
let useOptimistic;
26+
let useFormState;
2627

2728
describe('ReactDOMFizzForm', () => {
2829
beforeEach(() => {
@@ -31,6 +32,7 @@ describe('ReactDOMFizzForm', () => {
3132
ReactDOMServer = require('react-dom/server.browser');
3233
ReactDOMClient = require('react-dom/client');
3334
useFormStatus = require('react-dom').experimental_useFormStatus;
35+
useFormState = require('react-dom').experimental_useFormState;
3436
useOptimistic = require('react').experimental_useOptimistic;
3537
act = require('internal-test-utils').act;
3638
container = document.createElement('div');
@@ -470,6 +472,28 @@ describe('ReactDOMFizzForm', () => {
470472
expect(container.textContent).toBe('hi');
471473
});
472474

475+
// @gate enableFormActions
476+
// @gate enableAsyncActions
477+
it('useFormState returns initial state', async () => {
478+
async function action(state) {
479+
return state;
480+
}
481+
482+
function App() {
483+
const [state] = useFormState(action, 0);
484+
return state;
485+
}
486+
487+
const stream = await ReactDOMServer.renderToReadableStream(<App />);
488+
await readIntoContainer(stream);
489+
expect(container.textContent).toBe('0');
490+
491+
await act(async () => {
492+
ReactDOMClient.hydrateRoot(container, <App />);
493+
});
494+
expect(container.textContent).toBe('0');
495+
});
496+
473497
// @gate enableFormActions
474498
it('can provide a custom action on the server for actions', async () => {
475499
const ref = React.createRef();

packages/react-dom/src/__tests__/ReactDOMForm-test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe('ReactDOMForm', () => {
4040
let startTransition;
4141
let textCache;
4242
let useFormStatus;
43+
let useFormState;
4344

4445
beforeEach(() => {
4546
jest.resetModules();
@@ -53,6 +54,7 @@ describe('ReactDOMForm', () => {
5354
Suspense = React.Suspense;
5455
startTransition = React.startTransition;
5556
useFormStatus = ReactDOM.experimental_useFormStatus;
57+
useFormState = ReactDOM.experimental_useFormState;
5658
container = document.createElement('div');
5759
document.body.appendChild(container);
5860

@@ -969,4 +971,24 @@ describe('ReactDOMForm', () => {
969971
'A React form was unexpectedly submitted. If you called form.submit()',
970972
);
971973
});
974+
975+
// @gate enableFormActions
976+
// @gate enableAsyncActions
977+
test('useFormState exists', async () => {
978+
// TODO: Not yet implemented. This just tests that the API is wired up.
979+
980+
async function action(state) {
981+
return state;
982+
}
983+
984+
function App() {
985+
const [state] = useFormState(action, 0);
986+
return <Text text={state} />;
987+
}
988+
989+
const root = ReactDOMClient.createRoot(container);
990+
await act(() => root.render(<App />));
991+
assertLog([0]);
992+
expect(container.textContent).toBe('0');
993+
});
972994
});

packages/react-dom/src/client/ReactDOM.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ export {
6363
preinit,
6464
preinitModule,
6565
} from '../shared/ReactDOMFloat';
66-
export {useFormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
66+
export {
67+
useFormStatus,
68+
useFormState,
69+
} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
6770

6871
if (__DEV__) {
6972
if (

0 commit comments

Comments
 (0)