From 919620b2935ca6bb8dfc96204a7cf8754c006240 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 24 Apr 2023 20:18:34 -0400 Subject: [PATCH] Add stub for experimental_useFormStatus (#26719) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/react-dom/index.classic.fb.js | 1 + packages/react-dom/index.experimental.js | 1 + packages/react-dom/index.js | 1 + packages/react-dom/index.modern.fb.js | 1 + packages/react-dom/src/ReactDOMFormActions.js | 50 +++++++++++++++++++ .../src/__tests__/ReactDOMFizzForm-test.js | 18 +++++++ .../src/__tests__/ReactDOMForm-test.js | 18 +++++++ packages/react-dom/src/client/ReactDOM.js | 1 + 8 files changed, 91 insertions(+) create mode 100644 packages/react-dom/src/ReactDOMFormActions.js diff --git a/packages/react-dom/index.classic.fb.js b/packages/react-dom/index.classic.fb.js index a9efae8208b94..e093e3ff08499 100644 --- a/packages/react-dom/index.classic.fb.js +++ b/packages/react-dom/index.classic.fb.js @@ -31,6 +31,7 @@ export { unstable_createEventHandle, unstable_renderSubtreeIntoContainer, unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority. + useFormStatus as experimental_useFormStatus, prefetchDNS, preconnect, preload, diff --git a/packages/react-dom/index.experimental.js b/packages/react-dom/index.experimental.js index 5e905c19a6f57..32d66c66b3626 100644 --- a/packages/react-dom/index.experimental.js +++ b/packages/react-dom/index.experimental.js @@ -20,6 +20,7 @@ export { unstable_batchedUpdates, unstable_renderSubtreeIntoContainer, unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority. + useFormStatus as experimental_useFormStatus, prefetchDNS, preconnect, preload, diff --git a/packages/react-dom/index.js b/packages/react-dom/index.js index 169db31142d9b..d9290ae6099f2 100644 --- a/packages/react-dom/index.js +++ b/packages/react-dom/index.js @@ -23,6 +23,7 @@ export { unstable_createEventHandle, unstable_renderSubtreeIntoContainer, unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority. + useFormStatus as experimental_useFormStatus, prefetchDNS, preconnect, preload, diff --git a/packages/react-dom/index.modern.fb.js b/packages/react-dom/index.modern.fb.js index c41519668d5a5..6ea394c8179d5 100644 --- a/packages/react-dom/index.modern.fb.js +++ b/packages/react-dom/index.modern.fb.js @@ -16,6 +16,7 @@ export { unstable_batchedUpdates, unstable_createEventHandle, unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority. + useFormStatus as experimental_useFormStatus, prefetchDNS, preconnect, preload, diff --git a/packages/react-dom/src/ReactDOMFormActions.js b/packages/react-dom/src/ReactDOMFormActions.js new file mode 100644 index 0000000000000..b5a763976d03f --- /dev/null +++ b/packages/react-dom/src/ReactDOMFormActions.js @@ -0,0 +1,50 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {enableAsyncActions, enableFormActions} from 'shared/ReactFeatureFlags'; + +type FormStatusNotPending = {| + pending: false, + data: null, + method: null, + action: null, +|}; + +type FormStatusPending = {| + pending: true, + data: FormData, + method: string, + action: string | (FormData => void | Promise), +|}; + +export type FormStatus = FormStatusPending | FormStatusNotPending; + +// Since the "not pending" value is always the same, we can reuse the +// same object across all transitions. +const sharedNotPendingObject = { + pending: false, + data: null, + method: null, + action: null, +}; + +const NotPending: FormStatus = __DEV__ + ? Object.freeze(sharedNotPendingObject) + : sharedNotPendingObject; + +export function useFormStatus(): FormStatus { + if (!(enableFormActions && enableAsyncActions)) { + throw new Error('Not implemented.'); + } else { + // TODO: This isn't fully implemented yet but we return a correctly typed + // value so we can test that the API is exposed and gated correctly. The + // real implementation will access the status via the dispatcher. + return NotPending; + } +} diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index efb3a1de960e8..900f270b9fba5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -19,6 +19,7 @@ let container; let React; let ReactDOMServer; let ReactDOMClient; +let useFormStatus; describe('ReactDOMFizzForm', () => { beforeEach(() => { @@ -26,6 +27,7 @@ describe('ReactDOMFizzForm', () => { React = require('react'); ReactDOMServer = require('react-dom/server.browser'); ReactDOMClient = require('react-dom/client'); + useFormStatus = require('react-dom').experimental_useFormStatus; act = require('internal-test-utils').act; container = document.createElement('div'); document.body.appendChild(container); @@ -360,4 +362,20 @@ describe('ReactDOMFizzForm', () => { expect(buttonRef.current.hasAttribute('formMethod')).toBe(false); expect(buttonRef.current.hasAttribute('formTarget')).toBe(false); }); + + // @gate enableFormActions + // @gate enableAsyncActions + it('useFormStatus is not pending during server render', async () => { + function App() { + const {pending} = useFormStatus(); + return 'Pending: ' + pending; + } + + const stream = await ReactDOMServer.renderToReadableStream(); + await readIntoContainer(stream); + expect(container.textContent).toBe('Pending: false'); + + await act(() => ReactDOMClient.hydrateRoot(container, )); + expect(container.textContent).toBe('Pending: false'); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 267d19410bdd2..ba312e9dd66d9 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -39,6 +39,7 @@ describe('ReactDOMForm', () => { let Suspense; let startTransition; let textCache; + let useFormStatus; beforeEach(() => { jest.resetModules(); @@ -51,6 +52,7 @@ describe('ReactDOMForm', () => { useState = React.useState; Suspense = React.Suspense; startTransition = React.startTransition; + useFormStatus = ReactDOM.experimental_useFormStatus; container = document.createElement('div'); document.body.appendChild(container); @@ -846,4 +848,20 @@ describe('ReactDOMForm', () => { assertLog(['Oh no!', 'Oh no!']); expect(container.textContent).toBe('Oh no!'); }); + + // @gate enableFormActions + // @gate enableAsyncActions + it('useFormStatus exists', async () => { + // This API isn't fully implemented yet. This just tests that it's wired + // up correctly. + + function App() { + const {pending} = useFormStatus(); + return 'Pending: ' + pending; + } + + const root = ReactDOMClient.createRoot(container); + await act(() => root.render()); + expect(container.textContent).toBe('Pending: false'); + }); }); diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index d6ef4261246de..f31dcee5bae1e 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -56,6 +56,7 @@ import { import Internals from '../ReactDOMSharedInternals'; export {prefetchDNS, preconnect, preload, preinit} from '../ReactDOMFloat'; +export {useFormStatus} from '../ReactDOMFormActions'; if (__DEV__) { if (