-
Notifications
You must be signed in to change notification settings - Fork 47.5k
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
Add React.useActionState
#28491
Add React.useActionState
#28491
Conversation
1d4e7f9
to
8fd1e99
Compare
In your PR description, what is the |
Good call @sophiebits, I was thinking it it's null you would do something with it like handle a re-direct, but in the other examples the action would handle that for you so it makes sense to drop it here. Nice catch @meghsohor, fixed and I re-formatted all the examples to fix the syntax errors and formatting. |
Reading between the lines a bit here, could this be used allow you to opt out of Next.js' serial server action processing? |
54ab3ab
to
651adba
Compare
@tom-sherman I'm not familiar with the Next processing for server actions, but I don't think anything about this would change their implementation and that wasn't a motivating factor. It's the same hook, with a different name, a pending value, and just moved to the |
In case anyone is reading the PR description examples and wondering (like I did) what types the first returned array element can be ( It looks like the first argument is just react/packages/react-debug-tools/src/ReactDebugHooks.js Lines 656 to 659 in 651adba
Also confirmed over here:
Source: https://twitter.com/rickhanlonii/status/1765592038026641571 |
Thanks @karlhorky, I added the type to the reference section, updated the code examples to use |
): [Awaited<S>, (P) => void, boolean] { | ||
currentHookNameInDev = 'useActionState'; | ||
mountHookTypesDev(); | ||
return mountFormState(action, initialState, permalink); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Let's rename these to "action state" since the form state ones will go away eventually
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to do that in a follow up, it was in the original version but the PR was too big, so I'm splitting it out. Wanted to get this reviewed before submitting that to avoid annoying conflicts.
651adba
to
435e9b3
Compare
435e9b3
to
a31a3df
Compare
Great PR and great to see how fast the team responds to the feedback! Leaving just my 2 cents here from an educational perspective: I ran into the same confusion when I wrote my blog post about Forms in Next. Now if I understand this PR correctly, Now I have a kinda related question. In my article, I wanted to show how to trigger a reactive toast message once an action returns its response. But I had no indicator for the new
Since the returned
Now I see there would be a way around this with |
@malyzeli Thanks, fixed. |
Next.js hasn't caught up with the React version that supports this hook. You need to wait for a release of vercel/next.js#64798 to use this hook. |
Excellent article you published, really a detailed way of explanation, waiting for an update. Thank you |
great work. Cant wait to use it w/ NextJS. I really like that the pending is merged into one hook (apart from other great things). Came here to see if useFormState can also be triggered manually within a react-hook-form handler just to see that the next iteration is even better. One little bonus of the rename is that there is no name clash with the hook from react-hook-form anymore. I know that was not the reason but a nice sideeffect. |
Thank you for adding this new hook—I'm really excited about it. async function serverAction(arg1, arg2, arg3) {} |
Hey @sebmarkbage, thanks for getting back to me above and apologies for not replying back until now. Before receiving your response, I created this reference repo where I highlighted the aforementioned issues in a practical example. Happy I still don't see the purpose of the first "last state"-argument on the action handlers - anything that it does solve for me in my app can be solved by adding a hidden input, but I'm looking forward to seeing these advanced use cases. I did ask Twitter to see if anyone could give me a compelling reason why it exists, but I received no compelling responses, so I and others seem to need some education. Currently, it feels like a leaky abstraction that should be hidden from me as a user. Also before receiving your response, I created a bastardized version of the API I suggested above in this PR of the repo above by hacking Next.js' async storage and using a global context provider. I did it by hydrating the submitted form's values (with omitted files) and I think it's worth the trade-off, considering it also would mean each proper "with JS" submission can always return zero bytes for the previous payload since it can simply be provided by sniffing I see that "good forms" are per definition "controlled forms" (and for big/complex forms it'd always be the case), but if the APIs would allow it, I think uncontrolled forms could become great forms. The APIs still leave me wanting a lil' bit more, but I'm grateful for your work and your response to this; I know there are many considerations and I know that I still might be missing important nuances. Thank you. 🙏 |
Thanks |
In the pull request above was text:
suggesting we can use action returned by
Current docs also seem to suggest you can use it only within form. This has caused some confusion. Can someone please shed some light on this? |
@gmoniava Have you tried wrapping it in |
@eps1lon that is not the problem though I was interested why there is still that warning when the pull request here says it is ok to use |
It is ok to use it outside forms. Just within |
#28491 (comment) DiffTrain build for [b65afdd](b65afdd)
#28491 (comment) DiffTrain build for [b65afdd](b65afdd)
@eps1lon it would be nice if docs mention explicitly if it is allowed to call server functions in client components directly without wrapping them in transitions, because even Nextjs docs have examples of calling actions directly:
Current react docs suggest transitions in such cases but maybe would be better to highlight what happens if we call them directly. |
Overview
Depends on #28514
This PR adds a new React hook called
useActionState
to replace and improve the ReactDOMuseFormState
hook.Motivation
This hook intends to fix some of the confusion and limitations of the
useFormState
hook.The
useFormState
hook is only exported from theReactDOM
package and implies that it is used only for the state of<form>
actions, similar touseFormStatus
(which is only for<form>
element status). This leads to understandable confusion about whyuseFormState
does not provide apending
state value likeuseFormStatus
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 touseFormState
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 returnedaction
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 onlyreact-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:
useFormState
touseActionState
pending
state to the returned tuple'react'
packageReference
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 optionalinitialState
value used for the initial render, and an optionalpermalink
argument for renderer specific pre-hydration handling (such as a URL to support progressive hydration inreact-dom
).Type:
The hook returns a tuple with:
state
: the last state the action returneddispatch
: the method to call to dispatch the wrapped actionpending
: the pending state of the action and any state updates containedNotably, 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 touseFormState
:But it doesn't need to be used with a
<form/>
(neither diduseFormState
, hence the confusion):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:
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 touseTransition
. 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 byuseActionState
. 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:
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
:For greater control, you can also wrap both in a transition and use the
isPending
state of the transition:A similar technique using
useOptimistic
is preferred over usinguseTransition
directly, and is left as an exercise to the reader.Thanks
Thanks to @ryanflorence @mjackson @wesbos (#27980 (comment)) and Allan Lasser for their feedback and suggestions on
useFormStatus
hook.