Skip to content

Commit 919620b

Browse files
authored
Add stub for experimental_useFormStatus (#26719)
This wires up, but does not yet implement, an experimental hook called useFormStatus. The hook is imported from React DOM, not React, because it represents DOM-specific state — its return type includes FormData as one of its fields. Other renderers that implement similar methods would use their own renderer-specific types. The API is prefixed and only available in the experimental channel. It can only be used from client (browser, SSR) components, not Server Components.
1 parent 9ece58e commit 919620b

File tree

8 files changed

+91
-0
lines changed

8 files changed

+91
-0
lines changed

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

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

packages/react-dom/index.experimental.js

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

packages/react-dom/index.js

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export {
1616
unstable_batchedUpdates,
1717
unstable_createEventHandle,
1818
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
19+
useFormStatus as experimental_useFormStatus,
1920
prefetchDNS,
2021
preconnect,
2122
preload,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import {enableAsyncActions, enableFormActions} from 'shared/ReactFeatureFlags';
11+
12+
type FormStatusNotPending = {|
13+
pending: false,
14+
data: null,
15+
method: null,
16+
action: null,
17+
|};
18+
19+
type FormStatusPending = {|
20+
pending: true,
21+
data: FormData,
22+
method: string,
23+
action: string | (FormData => void | Promise<void>),
24+
|};
25+
26+
export type FormStatus = FormStatusPending | FormStatusNotPending;
27+
28+
// Since the "not pending" value is always the same, we can reuse the
29+
// same object across all transitions.
30+
const sharedNotPendingObject = {
31+
pending: false,
32+
data: null,
33+
method: null,
34+
action: null,
35+
};
36+
37+
const NotPending: FormStatus = __DEV__
38+
? Object.freeze(sharedNotPendingObject)
39+
: sharedNotPendingObject;
40+
41+
export function useFormStatus(): FormStatus {
42+
if (!(enableFormActions && enableAsyncActions)) {
43+
throw new Error('Not implemented.');
44+
} else {
45+
// TODO: This isn't fully implemented yet but we return a correctly typed
46+
// value so we can test that the API is exposed and gated correctly. The
47+
// real implementation will access the status via the dispatcher.
48+
return NotPending;
49+
}
50+
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ let container;
1919
let React;
2020
let ReactDOMServer;
2121
let ReactDOMClient;
22+
let useFormStatus;
2223

2324
describe('ReactDOMFizzForm', () => {
2425
beforeEach(() => {
2526
jest.resetModules();
2627
React = require('react');
2728
ReactDOMServer = require('react-dom/server.browser');
2829
ReactDOMClient = require('react-dom/client');
30+
useFormStatus = require('react-dom').experimental_useFormStatus;
2931
act = require('internal-test-utils').act;
3032
container = document.createElement('div');
3133
document.body.appendChild(container);
@@ -360,4 +362,20 @@ describe('ReactDOMFizzForm', () => {
360362
expect(buttonRef.current.hasAttribute('formMethod')).toBe(false);
361363
expect(buttonRef.current.hasAttribute('formTarget')).toBe(false);
362364
});
365+
366+
// @gate enableFormActions
367+
// @gate enableAsyncActions
368+
it('useFormStatus is not pending during server render', async () => {
369+
function App() {
370+
const {pending} = useFormStatus();
371+
return 'Pending: ' + pending;
372+
}
373+
374+
const stream = await ReactDOMServer.renderToReadableStream(<App />);
375+
await readIntoContainer(stream);
376+
expect(container.textContent).toBe('Pending: false');
377+
378+
await act(() => ReactDOMClient.hydrateRoot(container, <App />));
379+
expect(container.textContent).toBe('Pending: false');
380+
});
363381
});

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ describe('ReactDOMForm', () => {
3939
let Suspense;
4040
let startTransition;
4141
let textCache;
42+
let useFormStatus;
4243

4344
beforeEach(() => {
4445
jest.resetModules();
@@ -51,6 +52,7 @@ describe('ReactDOMForm', () => {
5152
useState = React.useState;
5253
Suspense = React.Suspense;
5354
startTransition = React.startTransition;
55+
useFormStatus = ReactDOM.experimental_useFormStatus;
5456
container = document.createElement('div');
5557
document.body.appendChild(container);
5658

@@ -846,4 +848,20 @@ describe('ReactDOMForm', () => {
846848
assertLog(['Oh no!', 'Oh no!']);
847849
expect(container.textContent).toBe('Oh no!');
848850
});
851+
852+
// @gate enableFormActions
853+
// @gate enableAsyncActions
854+
it('useFormStatus exists', async () => {
855+
// This API isn't fully implemented yet. This just tests that it's wired
856+
// up correctly.
857+
858+
function App() {
859+
const {pending} = useFormStatus();
860+
return 'Pending: ' + pending;
861+
}
862+
863+
const root = ReactDOMClient.createRoot(container);
864+
await act(() => root.render(<App />));
865+
expect(container.textContent).toBe('Pending: false');
866+
});
849867
});

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
import Internals from '../ReactDOMSharedInternals';
5757

5858
export {prefetchDNS, preconnect, preload, preinit} from '../ReactDOMFloat';
59+
export {useFormStatus} from '../ReactDOMFormActions';
5960

6061
if (__DEV__) {
6162
if (

0 commit comments

Comments
 (0)