-
Notifications
You must be signed in to change notification settings - Fork 47k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
useFormStatus only works from within the scope of the form it relates to #27980
Comments
Agreed, if HTML allows us to connect the submit button and form together via an id, then the useFormStatus Hook should also allow us to do the same thing. |
Article about this problem: https://allanlasser.com/posts/2024-01-26-avoid-using-reacts-useformstatus |
Note that useFormStatus is a high level helper mainly intended for quick iteration. The same features can be implemented with onSubmit + startTransition + useOptimistic. Which is really how I’d do any rich complex form. |
Good to know! Not knowing the hook's source code, I was unaware that it's composed from other APIs. If the |
This is exactly the question I wanted to ask...thumbs up for the initiative |
I'm sure there is a valid reason, but can anyone explain why the I would love to just have a form and know it's state and status in a single component |
While it wouldn't make sense for the In fact, you can just build a Hook around In theory The built-ins in React is more about mapping DOM primitives than building abstractions out-of-the-box. So it would still make sense to maybe wrap So |
Sounds like between #6620 eventually getting merged and the emergence of Either:
OR
|
## Overview _Depends on https://github.com/facebook/react/pull/28514_ This PR adds a new React hook called `useActionState` to replace and improve the ReactDOM `useFormState` hook. ## Motivation This hook intends to fix some of the confusion and limitations of the `useFormState` hook. The `useFormState` hook is only exported from the `ReactDOM` package and implies that it is used only for the state of `<form>` actions, similar to `useFormStatus` (which is only for `<form>` element status). This leads to understandable confusion about why `useFormState` does not provide a `pending` state value like `useFormStatus` does. The key insight is that the `useFormState` hook does not actually return the state of any particular form at all. Instead, it returns the state of the _action_ passed to the hook, wrapping it and returning a trackable action to add to a form, and returning the last returned value of the action given. In fact, `useFormState` doesn't need to be used in a `<form>` at all. Thus, adding a `pending` value to `useFormState` as-is would thus be confusing because it would only return the pending state of the _action_ given, not the `<form>` the action is passed to. Even if we wanted to tie them together, the returned `action` can be passed to multiple forms, creating confusing and conflicting pending states during multiple form submissions. Additionally, since the action is not related to any particular `<form>`, the hook can be used in any renderer - not only `react-dom`. For example, React Native could use the hook to wrap an action, pass it to a component that will unwrap it, and return the form result state and pending state. It's renderer agnostic. To fix these issues, this PR: - Renames `useFormState` to `useActionState` - Adds a `pending` state to the returned tuple - Moves the hook to the `'react'` package ## Reference The `useFormState` hook allows you to track the pending state and return value of a function (called an "action"). The function passed can be a plain JavaScript client function, or a bound server action to a reference on the server. It accepts an optional `initialState` value used for the initial render, and an optional `permalink` argument for renderer specific pre-hydration handling (such as a URL to support progressive hydration in `react-dom`). Type: ```ts function useActionState<State>( action: (state: Awaited<State>) => State | Promise<State>, initialState: Awaited<State>, permalink?: string, ): [state: Awaited<State>, dispatch: () => void, boolean]; ``` The hook returns a tuple with: - `state`: the last state the action returned - `dispatch`: the method to call to dispatch the wrapped action - `pending`: the pending state of the action and any state updates contained Notably, state updates inside of the action dispatched are wrapped in a transition to keep the page responsive while the action is completing and the UI is updated based on the result. ## Usage The `useActionState` hook can be used similar to `useFormState`: ```js import { useActionState } from "react"; // not react-dom function Form({ formAction }) { const [state, action, isPending] = useActionState(formAction); return ( <form action={action}> <input type="email" name="email" disabled={isPending} /> <button type="submit" disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </form> ); } ``` But it doesn't need to be used with a `<form/>` (neither did `useFormState`, hence the confusion): ```js import { useActionState, useRef } from "react"; function Form({ someAction }) { const ref = useRef(null); const [state, action, isPending] = useActionState(someAction); async function handleSubmit() { // See caveats below await action({ email: ref.current.value }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` ## Benefits One of the benefits of using this hook is the automatic tracking of the return value and pending states of the wrapped function. For example, the above example could be accomplished via: ```js import { useActionState, useRef } from "react"; function Form({ someAction }) { const ref = useRef(null); const [state, setState] = useState(null); const [isPending, setIsPending] = useTransition(); function handleSubmit() { startTransition(async () => { const response = await someAction({ email: ref.current.value }); setState(response); }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` However, this hook adds more benefits when used with render specific elements like react-dom `<form>` elements and Server Action. With `<form>` elements, React will automatically support replay actions on the form if it is submitted before hydration has completed, providing a form of partial progressive enhancement: enhancement for when javascript is enabled but not ready. Additionally, with the `permalink` argument and Server Actions, frameworks can provide full progressive enhancement support, submitting the form to the URL provided along with the FormData from the form. On submission, the Server Action will be called during the MPA navigation, similar to any raw HTML app, server rendered, and the result returned to the client without any JavaScript on the client. ## Caveats There are a few Caveats to this new hook: **Additional state update**: Since we cannot know whether you use the pending state value returned by the hook, the hook will always set the `isPending` state at the beginning of the first chained action, resulting in an additional state update similar to `useTransition`. In the future a type-aware compiler could optimize this for when the pending state is not accessed. **Pending state is for the action, not the handler**: The difference is subtle but important, the pending state begins when the return action is dispatched and will revert back after all actions and transitions have settled. The mechanism for this under the hook is the same as useOptimisitic. Concretely, what this means is that the pending state of `useActionState` will not represent any actions or sync work performed before dispatching the action returned by `useActionState`. Hopefully this is obvious based on the name and shape of the API, but there may be some temporary confusion. As an example, let's take the above example and await another action inside of it: ```js import { useActionState, useRef } from "react"; function Form({ someAction, someOtherAction }) { const ref = useRef(null); const [state, action, isPending] = useActionState(someAction); async function handleSubmit() { await someOtherAction(); // The pending state does not start until this call. await action({ email: ref.current.value }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` Since the pending state is related to the action, and not the handler or form it's attached to, the pending state only changes when the action is dispatched. To solve, there are two options. First (recommended): place the other function call inside of the action passed to `useActionState`: ```js import { useActionState, useRef } from "react"; function Form({ someAction, someOtherAction }) { const ref = useRef(null); const [state, action, isPending] = useActionState(async (data) => { // Pending state is true already. await someOtherAction(); return someAction(data); }); async function handleSubmit() { // The pending state starts at this call. await action({ email: ref.current.value }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` For greater control, you can also wrap both in a transition and use the `isPending` state of the transition: ```js import { useActionState, useTransition, useRef } from "react"; function Form({ someAction, someOtherAction }) { const ref = useRef(null); // isPending is used from the transition wrapping both action calls. const [isPending, startTransition] = useTransition(); // isPending not used from the individual action. const [state, action] = useActionState(someAction); async function handleSubmit() { startTransition(async () => { // The transition pending state has begun. await someOtherAction(); await action({ email: ref.current.value }); }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` A similar technique using `useOptimistic` is preferred over using `useTransition` directly, and is left as an exercise to the reader. ## Thanks Thanks to @ryanflorence @mjackson @wesbos (#27980 (comment)) and [Allan Lasser](https://allanlasser.com/posts/2024-01-26-avoid-using-reacts-useformstatus) for their feedback and suggestions on `useFormStatus` hook.
## Overview _Depends on https://github.com/facebook/react/pull/28514_ This PR adds a new React hook called `useActionState` to replace and improve the ReactDOM `useFormState` hook. ## Motivation This hook intends to fix some of the confusion and limitations of the `useFormState` hook. The `useFormState` hook is only exported from the `ReactDOM` package and implies that it is used only for the state of `<form>` actions, similar to `useFormStatus` (which is only for `<form>` element status). This leads to understandable confusion about why `useFormState` does not provide a `pending` state value like `useFormStatus` does. The key insight is that the `useFormState` hook does not actually return the state of any particular form at all. Instead, it returns the state of the _action_ passed to the hook, wrapping it and returning a trackable action to add to a form, and returning the last returned value of the action given. In fact, `useFormState` doesn't need to be used in a `<form>` at all. Thus, adding a `pending` value to `useFormState` as-is would thus be confusing because it would only return the pending state of the _action_ given, not the `<form>` the action is passed to. Even if we wanted to tie them together, the returned `action` can be passed to multiple forms, creating confusing and conflicting pending states during multiple form submissions. Additionally, since the action is not related to any particular `<form>`, the hook can be used in any renderer - not only `react-dom`. For example, React Native could use the hook to wrap an action, pass it to a component that will unwrap it, and return the form result state and pending state. It's renderer agnostic. To fix these issues, this PR: - Renames `useFormState` to `useActionState` - Adds a `pending` state to the returned tuple - Moves the hook to the `'react'` package ## Reference The `useFormState` hook allows you to track the pending state and return value of a function (called an "action"). The function passed can be a plain JavaScript client function, or a bound server action to a reference on the server. It accepts an optional `initialState` value used for the initial render, and an optional `permalink` argument for renderer specific pre-hydration handling (such as a URL to support progressive hydration in `react-dom`). Type: ```ts function useActionState<State>( action: (state: Awaited<State>) => State | Promise<State>, initialState: Awaited<State>, permalink?: string, ): [state: Awaited<State>, dispatch: () => void, boolean]; ``` The hook returns a tuple with: - `state`: the last state the action returned - `dispatch`: the method to call to dispatch the wrapped action - `pending`: the pending state of the action and any state updates contained Notably, state updates inside of the action dispatched are wrapped in a transition to keep the page responsive while the action is completing and the UI is updated based on the result. ## Usage The `useActionState` hook can be used similar to `useFormState`: ```js import { useActionState } from "react"; // not react-dom function Form({ formAction }) { const [state, action, isPending] = useActionState(formAction); return ( <form action={action}> <input type="email" name="email" disabled={isPending} /> <button type="submit" disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </form> ); } ``` But it doesn't need to be used with a `<form/>` (neither did `useFormState`, hence the confusion): ```js import { useActionState, useRef } from "react"; function Form({ someAction }) { const ref = useRef(null); const [state, action, isPending] = useActionState(someAction); async function handleSubmit() { // See caveats below await action({ email: ref.current.value }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` ## Benefits One of the benefits of using this hook is the automatic tracking of the return value and pending states of the wrapped function. For example, the above example could be accomplished via: ```js import { useActionState, useRef } from "react"; function Form({ someAction }) { const ref = useRef(null); const [state, setState] = useState(null); const [isPending, setIsPending] = useTransition(); function handleSubmit() { startTransition(async () => { const response = await someAction({ email: ref.current.value }); setState(response); }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` However, this hook adds more benefits when used with render specific elements like react-dom `<form>` elements and Server Action. With `<form>` elements, React will automatically support replay actions on the form if it is submitted before hydration has completed, providing a form of partial progressive enhancement: enhancement for when javascript is enabled but not ready. Additionally, with the `permalink` argument and Server Actions, frameworks can provide full progressive enhancement support, submitting the form to the URL provided along with the FormData from the form. On submission, the Server Action will be called during the MPA navigation, similar to any raw HTML app, server rendered, and the result returned to the client without any JavaScript on the client. ## Caveats There are a few Caveats to this new hook: **Additional state update**: Since we cannot know whether you use the pending state value returned by the hook, the hook will always set the `isPending` state at the beginning of the first chained action, resulting in an additional state update similar to `useTransition`. In the future a type-aware compiler could optimize this for when the pending state is not accessed. **Pending state is for the action, not the handler**: The difference is subtle but important, the pending state begins when the return action is dispatched and will revert back after all actions and transitions have settled. The mechanism for this under the hook is the same as useOptimisitic. Concretely, what this means is that the pending state of `useActionState` will not represent any actions or sync work performed before dispatching the action returned by `useActionState`. Hopefully this is obvious based on the name and shape of the API, but there may be some temporary confusion. As an example, let's take the above example and await another action inside of it: ```js import { useActionState, useRef } from "react"; function Form({ someAction, someOtherAction }) { const ref = useRef(null); const [state, action, isPending] = useActionState(someAction); async function handleSubmit() { await someOtherAction(); // The pending state does not start until this call. await action({ email: ref.current.value }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` Since the pending state is related to the action, and not the handler or form it's attached to, the pending state only changes when the action is dispatched. To solve, there are two options. First (recommended): place the other function call inside of the action passed to `useActionState`: ```js import { useActionState, useRef } from "react"; function Form({ someAction, someOtherAction }) { const ref = useRef(null); const [state, action, isPending] = useActionState(async (data) => { // Pending state is true already. await someOtherAction(); return someAction(data); }); async function handleSubmit() { // The pending state starts at this call. await action({ email: ref.current.value }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` For greater control, you can also wrap both in a transition and use the `isPending` state of the transition: ```js import { useActionState, useTransition, useRef } from "react"; function Form({ someAction, someOtherAction }) { const ref = useRef(null); // isPending is used from the transition wrapping both action calls. const [isPending, startTransition] = useTransition(); // isPending not used from the individual action. const [state, action] = useActionState(someAction); async function handleSubmit() { startTransition(async () => { // The transition pending state has begun. await someOtherAction(); await action({ email: ref.current.value }); }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` A similar technique using `useOptimistic` is preferred over using `useTransition` directly, and is left as an exercise to the reader. ## Thanks Thanks to @ryanflorence @mjackson @wesbos (#27980 (comment)) and [Allan Lasser](https://allanlasser.com/posts/2024-01-26-avoid-using-reacts-useformstatus) for their feedback and suggestions on `useFormStatus` hook. DiffTrain build for [5c65b27](5c65b27)
## Overview _Depends on https://github.com/facebook/react/pull/28514_ This PR adds a new React hook called `useActionState` to replace and improve the ReactDOM `useFormState` hook. ## Motivation This hook intends to fix some of the confusion and limitations of the `useFormState` hook. The `useFormState` hook is only exported from the `ReactDOM` package and implies that it is used only for the state of `<form>` actions, similar to `useFormStatus` (which is only for `<form>` element status). This leads to understandable confusion about why `useFormState` does not provide a `pending` state value like `useFormStatus` does. The key insight is that the `useFormState` hook does not actually return the state of any particular form at all. Instead, it returns the state of the _action_ passed to the hook, wrapping it and returning a trackable action to add to a form, and returning the last returned value of the action given. In fact, `useFormState` doesn't need to be used in a `<form>` at all. Thus, adding a `pending` value to `useFormState` as-is would thus be confusing because it would only return the pending state of the _action_ given, not the `<form>` the action is passed to. Even if we wanted to tie them together, the returned `action` can be passed to multiple forms, creating confusing and conflicting pending states during multiple form submissions. Additionally, since the action is not related to any particular `<form>`, the hook can be used in any renderer - not only `react-dom`. For example, React Native could use the hook to wrap an action, pass it to a component that will unwrap it, and return the form result state and pending state. It's renderer agnostic. To fix these issues, this PR: - Renames `useFormState` to `useActionState` - Adds a `pending` state to the returned tuple - Moves the hook to the `'react'` package ## Reference The `useFormState` hook allows you to track the pending state and return value of a function (called an "action"). The function passed can be a plain JavaScript client function, or a bound server action to a reference on the server. It accepts an optional `initialState` value used for the initial render, and an optional `permalink` argument for renderer specific pre-hydration handling (such as a URL to support progressive hydration in `react-dom`). Type: ```ts function useActionState<State>( action: (state: Awaited<State>) => State | Promise<State>, initialState: Awaited<State>, permalink?: string, ): [state: Awaited<State>, dispatch: () => void, boolean]; ``` The hook returns a tuple with: - `state`: the last state the action returned - `dispatch`: the method to call to dispatch the wrapped action - `pending`: the pending state of the action and any state updates contained Notably, state updates inside of the action dispatched are wrapped in a transition to keep the page responsive while the action is completing and the UI is updated based on the result. ## Usage The `useActionState` hook can be used similar to `useFormState`: ```js import { useActionState } from "react"; // not react-dom function Form({ formAction }) { const [state, action, isPending] = useActionState(formAction); return ( <form action={action}> <input type="email" name="email" disabled={isPending} /> <button type="submit" disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </form> ); } ``` But it doesn't need to be used with a `<form/>` (neither did `useFormState`, hence the confusion): ```js import { useActionState, useRef } from "react"; function Form({ someAction }) { const ref = useRef(null); const [state, action, isPending] = useActionState(someAction); async function handleSubmit() { // See caveats below await action({ email: ref.current.value }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` ## Benefits One of the benefits of using this hook is the automatic tracking of the return value and pending states of the wrapped function. For example, the above example could be accomplished via: ```js import { useActionState, useRef } from "react"; function Form({ someAction }) { const ref = useRef(null); const [state, setState] = useState(null); const [isPending, setIsPending] = useTransition(); function handleSubmit() { startTransition(async () => { const response = await someAction({ email: ref.current.value }); setState(response); }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` However, this hook adds more benefits when used with render specific elements like react-dom `<form>` elements and Server Action. With `<form>` elements, React will automatically support replay actions on the form if it is submitted before hydration has completed, providing a form of partial progressive enhancement: enhancement for when javascript is enabled but not ready. Additionally, with the `permalink` argument and Server Actions, frameworks can provide full progressive enhancement support, submitting the form to the URL provided along with the FormData from the form. On submission, the Server Action will be called during the MPA navigation, similar to any raw HTML app, server rendered, and the result returned to the client without any JavaScript on the client. ## Caveats There are a few Caveats to this new hook: **Additional state update**: Since we cannot know whether you use the pending state value returned by the hook, the hook will always set the `isPending` state at the beginning of the first chained action, resulting in an additional state update similar to `useTransition`. In the future a type-aware compiler could optimize this for when the pending state is not accessed. **Pending state is for the action, not the handler**: The difference is subtle but important, the pending state begins when the return action is dispatched and will revert back after all actions and transitions have settled. The mechanism for this under the hook is the same as useOptimisitic. Concretely, what this means is that the pending state of `useActionState` will not represent any actions or sync work performed before dispatching the action returned by `useActionState`. Hopefully this is obvious based on the name and shape of the API, but there may be some temporary confusion. As an example, let's take the above example and await another action inside of it: ```js import { useActionState, useRef } from "react"; function Form({ someAction, someOtherAction }) { const ref = useRef(null); const [state, action, isPending] = useActionState(someAction); async function handleSubmit() { await someOtherAction(); // The pending state does not start until this call. await action({ email: ref.current.value }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` Since the pending state is related to the action, and not the handler or form it's attached to, the pending state only changes when the action is dispatched. To solve, there are two options. First (recommended): place the other function call inside of the action passed to `useActionState`: ```js import { useActionState, useRef } from "react"; function Form({ someAction, someOtherAction }) { const ref = useRef(null); const [state, action, isPending] = useActionState(async (data) => { // Pending state is true already. await someOtherAction(); return someAction(data); }); async function handleSubmit() { // The pending state starts at this call. await action({ email: ref.current.value }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` For greater control, you can also wrap both in a transition and use the `isPending` state of the transition: ```js import { useActionState, useTransition, useRef } from "react"; function Form({ someAction, someOtherAction }) { const ref = useRef(null); // isPending is used from the transition wrapping both action calls. const [isPending, startTransition] = useTransition(); // isPending not used from the individual action. const [state, action] = useActionState(someAction); async function handleSubmit() { startTransition(async () => { // The transition pending state has begun. await someOtherAction(); await action({ email: ref.current.value }); }); } return ( <div> <input ref={ref} type="email" name="email" disabled={isPending} /> <button onClick={handleSubmit} disabled={isPending}> Submit </button> {state.errorMessage && <p>{state.errorMessage}</p>} </div> ); } ``` A similar technique using `useOptimistic` is preferred over using `useTransition` directly, and is left as an exercise to the reader. ## Thanks Thanks to @ryanflorence @mjackson @wesbos (facebook#27980 (comment)) and [Allan Lasser](https://allanlasser.com/posts/2024-01-26-avoid-using-reacts-useformstatus) for their feedback and suggestions on `useFormStatus` hook.
As the docs stipulate [here]:
I understand this is limited as a pitfall, but I also think it's a pretty big limitation. It's possible to render a form's submit button outside of the scope of the form... anywhere on a page even... with the
form
attribute. I wanted to suggest that we should be offered the same affordances with this API, otherwise we are forced to render our markup differently to use React to the fullest extent.The text was updated successfully, but these errors were encountered: