mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Before this change, `useFormStatus` is only activated if a form is
submitted by an action function (either `<form action={actionFn}>` or
`<button formAction={actionFn}>`).
After this change, `useFormStatus` will also be activated if you call
`startTransition(actionFn)` inside a submit event handler that is
`preventDefault`-ed.
This is the last missing piece for implementing a custom `action` prop
that is progressively enhanced using `onSubmit` while maintaining the
same behavior as built-in form actions.
Here's the basic recipe for implementing a progressively-enhanced form
action. This would typically be implemented in your UI component
library, not regular application code:
```js
import {requestFormReset} from 'react-dom';
// To implement progressive enhancement, pass both a form action *and* a
// submit event handler. The action is used for submissions that happen
// before hydration, and the submit handler is used for submissions that
// happen after.
<form
action={action}
onSubmit={(event) => {
// After hydration, we upgrade the form with additional client-
// only behavior.
event.preventDefault();
// Manually dispatch the action.
startTransition(async () => {
// (Optional) Reset any uncontrolled inputs once the action is
// complete, like built-in form actions do.
requestFormReset(event.target);
// ...Do extra action-y stuff in here, like setting a custom
// optimistic state...
// Call the user-provided action
const formData = new FormData(event.target);
await action(formData);
});
}}
/>
```
96 lines
3.0 KiB
JavaScript
96 lines
3.0 KiB
JavaScript
/**
|
|
* 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 type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
|
|
import type {Awaited} from 'shared/ReactTypes';
|
|
|
|
import {enableAsyncActions} from 'shared/ReactFeatureFlags';
|
|
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
|
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
|
|
|
|
type FormStatusNotPending = {|
|
|
pending: false,
|
|
data: null,
|
|
method: null,
|
|
action: null,
|
|
|};
|
|
|
|
type FormStatusPending = {|
|
|
pending: true,
|
|
data: FormData,
|
|
method: string,
|
|
action: string | (FormData => void | Promise<void>) | null,
|
|
|};
|
|
|
|
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,
|
|
};
|
|
|
|
export const NotPending: FormStatus = __DEV__
|
|
? Object.freeze(sharedNotPendingObject)
|
|
: sharedNotPendingObject;
|
|
|
|
function resolveDispatcher() {
|
|
// Copied from react/src/ReactHooks.js. It's the same thing but in a
|
|
// different package.
|
|
const dispatcher = ReactSharedInternals.H;
|
|
if (__DEV__) {
|
|
if (dispatcher === null) {
|
|
console.error(
|
|
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
|
|
' one of the following reasons:\n' +
|
|
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
|
|
'2. You might be breaking the Rules of Hooks\n' +
|
|
'3. You might have more than one copy of React in the same app\n' +
|
|
'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
|
|
);
|
|
}
|
|
}
|
|
// Will result in a null access error if accessed outside render phase. We
|
|
// intentionally don't throw our own error because this is in a hot path.
|
|
// Also helps ensure this is inlined.
|
|
return ((dispatcher: any): Dispatcher);
|
|
}
|
|
|
|
export function useFormStatus(): FormStatus {
|
|
if (!enableAsyncActions) {
|
|
throw new Error('Not implemented.');
|
|
} else {
|
|
const dispatcher = resolveDispatcher();
|
|
// $FlowFixMe[not-a-function] We know this exists because of the feature check above.
|
|
return dispatcher.useHostTransitionStatus();
|
|
}
|
|
}
|
|
|
|
export function useFormState<S, P>(
|
|
action: (Awaited<S>, P) => S,
|
|
initialState: Awaited<S>,
|
|
permalink?: string,
|
|
): [Awaited<S>, (P) => void, boolean] {
|
|
if (!enableAsyncActions) {
|
|
throw new Error('Not implemented.');
|
|
} else {
|
|
const dispatcher = resolveDispatcher();
|
|
// $FlowFixMe[not-a-function] This is unstable, thus optional
|
|
return dispatcher.useFormState(action, initialState, permalink);
|
|
}
|
|
}
|
|
|
|
export function requestFormReset(form: HTMLFormElement) {
|
|
ReactDOMSharedInternals.d /* ReactDOMCurrentDispatcher */
|
|
.r(/* requestFormReset */ form);
|
|
}
|