Conversation
Size changesDetails📦 Next.js Bundle Analysis for react-devThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
MaxwellCohen
left a comment
There was a problem hiding this comment.
Much better than the current state at showing the value of UseActionState outside forms. UseActionState seems to be an async/actions version of useReducer, so adding more parallel language with the useReducer docs to show the value of useActionState.
Thank you for cleaning up these pages
| - **Use `useActionState`** to manage state of your Actions. The reducer can perform side effects. | ||
| You can think of `useActionState` as `useReducer` for side effects from user Actions. Since it computes the next Action to take based on the previous Action, it has to [order the calls sequentially](/reference/react/useActionState#how-useactionstate-queuing-works). If you want to perform Action in parallel, use `useState` and `useTransition` directly. |
There was a problem hiding this comment.
It would be really nice to have an example we can point to here. Maybe we could add one to the useTransition docs (and link it from here), or include a small snippet right here that shows what the “run these in parallel with useState + useTransition” pattern looks like
Kinda feels like we need a new term. I really like the existing docs on reducers and would eventually love to see a similar build-up of the logic for action reducers. Show how you can do it on your own, then show how useActionState basically provides sugar over that. What about asyncReducer? and If we eventually have a learn page around this stuff, we can explain the differences between "async reducers" and "reducers". asyncDispatch feels like a strong enough convention to hint that it must be called within a transition. And asyncReducer hints that "this is no ordinary reducer". This reducer can have side effects. |
|
In retrospect, and in light of your recent "Async React" branding (which I think is fantastic), maybe the hook could have been called useAsyncReducer ^_^ |
|
I wonder if "preserving a form's inputs after a failed submission" warrants its own section in Usage. It's such a common one and not entirely obvious how to use defaultValue to pull it off (given React 19 resets forms): import { login } from "./actions";
function Form() {
const [state, asyncDispatch, isPending] = useActionState(
async (prev, formData) => {
const { name, password } = Object.fromEntries(formData);
try {
await login(name, password);
return { status: "success" };
} catch (error) {
return { status: "error", error: error.toString(), formData };
}
},
{ status: "init" },
);
return (
<form action={asyncDispatch}>
<input
type="email"
name="email"
defaultValue={state.formData?.get("email") ?? ""}
/>
{state.status === "error" && <p>{state.error}</p>}
<input
type="password"
name="password"
defaultValue={state.formData?.get("password") ?? ""}
/>
</form>
);
} |
|
Another one that's not obvious is that you can mix async and sync code branches in the asyncReducer. Nice for resetting state or anything else that doesn't involve a side effect. async function updateCart(state, formData) {
const type = formData.get("type");
switch (type) {
case "ADD": {
return await addToCart(state.prevCount);
}
case "REMOVE": {
return await removeFromCart(state.prevCount);
}
case "RESET": {
return state.initialCount; // no async calls
}
default: {
throw Error("Unknown action: " + type);
}
}
}I've seen tons of folks in comments feeling like they're stuck with whatever state was returned from the previous server function, when they can just add a branch to reset a form all in the client. |
|
@samselikoff re: naming - the actions don't need to be async or could be a mix of sync and async. For example, you could have |
Yep I mentioned that above, but the fact that they can be async (and normal reducers cannot) seems to be a pretty crucial differentiator! |
| --- | ||
| ## Troubleshooting {/*troubleshooting*/} |
There was a problem hiding this comment.
Not sure if here is a good spot, but I think people tend to lean towards useEffect when using the state return value of useActionState. For example, this long thread on showing a toast. In the end, couldn't come up with a solution that worked with progressive enhancement without it. I think the crux of the issue might be trying to mix uncontrolled apis with it.
Maybe something more for the Learn section, not sure.
There was a problem hiding this comment.
I don't really understand what the issue there is - you can't show a toast if there's no JS. So the flow is: permalink before JS (no toast), then "upgrade" with JS (the useActionState in the tweet). Could you post a question in the Async React working group explaining the use case?
| - **Use `useActionState`** to manage state of your Actions. The reducer can perform side effects. | ||
| You can think of `useActionState` as `useReducer` for side effects from user Actions. Since it computes the next Action to take based on the previous Action, it has to [order the calls sequentially](/reference/react/useActionState#how-useactionstate-queuing-works). If you want to perform Action in parallel, use `useState` and `useTransition` directly. |
There was a problem hiding this comment.
If I were to summarize the main use cases of useActionState as 1) queing updates to resolve out of order updates and 2) progressive enhancement for showing feedback and navigating (only for forms), would that be correct?
I feel like the why behind it is too buried here and a summary of it (with some more focus on the problem it solves) near the beginning would be helpful.
I'm also curious, why was this hook created? Like what was the main problem it was trying to solve and which features are just coincidental or add-ons? I know we started with useFormState and then tried to generalize, but now the overlapping use cases and motivation make it all kinda fuzzy to me especially as usage with forms isn't emphasized as much.
PS: reading facebook/react#28491 now to see if it clicks.
also the "Save draft" example here: https://react.dev/reference/react-dom/components/form#handling-multiple-submission-types should be updated to not clear the input when saving the draft |
|
@samselikoff re: form action reset - I think we should add that to the form action docs here. The form action docs need a revamp of their own |
|
One thing that's maybe worth calling out after the first demo is that we'll add optimistic handling in another demo (linked). It feels very broken that there's no immediate feedback. |
| * `reducerAction` can be sync or async. It can perform sync actions like showing a notification, or async actions like posting updates to a server. | ||
| * `reducerAction` is not invoked twice in `<StrictMode>` since `reducerAction` is designed to allow side effects. | ||
| * The return type of `reducerAction` must match the type of `initialState`. If TypeScript infers a mismatch, you may need to explicitly annotate your state type. | ||
| * If you set state after `await` in the `reducerAction` you currently need to wrap the state update in an additional `startTransition`. See the [startTransition](/reference/react/useTransition#react-doesnt-treat-my-state-update-after-await-as-a-transition) docs for more info. |
There was a problem hiding this comment.
[Suggestion] Add something about when reducerAction returns a promise to this caveat, because reducerAction can return a promise or a value that does not have to be an async function. Since reducerAction can return both promises and the value directly is an amazing feature of this hook.
If you set state after `await` in the `reducerAction` or `reducerAction` returns a promise You need to wrap the state update in an additional `startTransition`. See the [startTransition](/reference/react/useTransition#react-doesnt-treat-my-state-update-after-await-as-a-transition) docs for more info.
I can see not adding the last line and going with the stance that reducerAction must be called in a transition, as it could cause confusion vs developers saving a little bit of code
edit: While listening to Ryan Carniato's stream Sync to Async (https://www.youtube.com/watch?v=drLX0yTKP04), I think that having the strict useTransition message is better for overall useablity so dont't have a line like ' If reducerAction is sync (returns a value directly), startTransition is not needed. "
|
@gaearon yeah good idea - but there is feedback right? the quantity and the total show a spinner (though really it should be on the button) |
|
Ah yeah, I just meant it feels very strange for a number to not increase |
|
Btw I'm also maybe team "async reducer".. |
|
Or maybe "action reducer" |
|
Yeah I can see that, especially in the second example more than the first. The naming
I also like how:
In other words, since Action means "side effect that is maybe async", it is a async reducer but that's only a subset. |
|
With the "async reducer" naming - what is the Action you're tracking the state of for |
|
Ok @gaearon I just pushed a change I'm pretty excited about, this feels clean af now. I updated all the pending states, so the feedback is better. More importantly, I moved the Which lets me explain this:
So the first three examples kinda build it up, and just when you think it's getting complicated, the Action prop example simplifies it all. |
|
The abortable section could be a re-usable hook btw: function useAbortableActionState(reducerAction, initialState) {
const abortRef = useRef(null);
const [state, dispatchAction, isPending] = useActionState(reducerAction, initialState);
async function wrappedAction(payload) {
if (abortRef.current) {
abortRef.current.abort();
}
abortRef.current = new AbortController();
await dispatchAction({ type: 'ADD', signal: abortRef.current.signal });
}
return [state, wrappedAction, isPending];
} |
|
We are missing a troubleshooting section for the following console errors (it might be implied, but not exact text).
|
| ### Using with Action props {/*using-with-action-props*/} | ||
| When you pass the `dispatchAction` function to a component that exposes an [Action prop](/reference/react/useTransition#exposing-action-props-from-components), you don't need to call `startTransition` or `useOptmisitc` yourself. |
There was a problem hiding this comment.
| When you pass the `dispatchAction` function to a component that exposes an [Action prop](/reference/react/useTransition#exposing-action-props-from-components), you don't need to call `startTransition` or `useOptmisitc` yourself. | |
| When you pass the `dispatchAction` function to a component that exposes an [Action prop](/reference/react/useTransition#exposing-action-props-from-components), you don't need to call `startTransition` or `useOptimistic` yourself. |
| `dispatchAction` must be called within a Transition. | ||
| You can wrap it in [`startTransition`](/reference/react/startTransition), or pass it to an [Action prop](/reference/react/useTransition#exposing-action-props-from-components). |
There was a problem hiding this comment.
Above, in the Returns section it was mentioned that:
A
dispatchActionfunction that you call inside Actions
I found a bit confusing that in this Note it is mentioned that a "dispatchAction must be called within a Transition."
I suggest something like:
| `dispatchAction` must be called within a Transition. | |
| You can wrap it in [`startTransition`](/reference/react/startTransition), or pass it to an [Action prop](/reference/react/useTransition#exposing-action-props-from-components). | |
| `dispatchAction` must be called from an Action (i.e. inside startTransition), or passed to an Action prop. | |
| Calls outside that scope won’t be treated as part of the Transition and log an error on development mode. |
| <Intro> | ||
|
|
||
| `useActionState` is a Hook that allows you to update state based on the result of a form action. | ||
| `useActionState` is a React Hook that lets you update state with side effects using [Actions](/reference/react/useTransition#functions-called-in-starttransition-are-called-actions). |
There was a problem hiding this comment.
nit: The "with side effects" threw me a bit until I realized (I think) that it's referring to the actionReducer being able to contain side effects?
Not sure of what's the best wording here, but yea, just wanted to leave my thoughts.
| #### Caveats {/*caveats*/} | ||
|
|
||
| * `useActionState` is a Hook, so you can only call it **at the top level of your component** or your own Hooks. You can't call it inside loops or conditions. If you need that, extract a new component and move the state into it. | ||
| * React queues and executes multiple calls to `dispatchAction` sequentially, allowing each `reducerAction` to use the result of the previous Action. |
There was a problem hiding this comment.
nit: "allowing each call to reducerAction to use..." or "allowing each reducerAction invocation to use..."?
"each reducerAction to use" seems like it's implying there are multiple reducerActions.
| } | ||
| ``` | ||
|
|
||
| Each time you call `dispatchAction`, React calls the `reducerAction` with the `actionPayload`. The reducer will perform side effects such as posting data, and return the new state. If `dispatchAction` is called multiple times, React queues and executes them in order so the result of the previous call is available for the current call. |
There was a problem hiding this comment.
nit: "available to the current call" or "available for the current call to use".
It just read a bit off.
|
|
||
| #### Parameters {/*reduceraction-parameters*/} | ||
|
|
||
| * `previousState`: The current state. Initially this is equal to the `initialState`. After the first call to `dispatchAction`, it's equal to the last state returned. |
There was a problem hiding this comment.
It did make me pause to see it called previousState and then immediately read "The current state". I get it, but did I have to think. In the new useOptimistic docs it says currentState for the optional reducer, but I know "previous" also tends to be used. Just kinda confusing to read it like this.
|
|
||
| * `previousState`: The current state. Initially this is equal to the `initialState`. After the first call to `dispatchAction`, it's equal to the last state returned. | ||
|
|
||
| * **optional** `actionPayload`: The argument passed to `dispatchAction`. It can be a value of any type. Similar to `useReducer` conventions, it is usually an object with a `type` property identifying it and, optionally, other properties with additional information. |
There was a problem hiding this comment.
You mention initialState needs to be serializable when using Server Functions, but the same also holds for actionPayload right? Should that be mentioned in the caveats section below?
|
|
||
| function handleDecrease() { | ||
| startTransition(async () => { | ||
| setOptimisticValue(c => c + 1); |
There was a problem hiding this comment.
Small bug.
| setOptimisticValue(c => c + 1); | |
| setOptimisticValue(c => c - 1); |
Also, I guess https://github.com/reactjs/react.dev/pull/8284/changes#r2776729030 is relevant here as well.
| When you use `useActionState`, the `reducerAction` receives an extra argument as its first argument: the previous or initial state. The submitted form data is therefore its second argument instead of its first. | ||
| ```js |
There was a problem hiding this comment.
Small thing, not sure.
| ```js | |
| ```js {2,7} |
|
|
||
| function handleRemove() { | ||
| startTransition(() => { | ||
| setOptimisticCount(c => c - 1); |
There was a problem hiding this comment.
nit: Maybe not worth the additional complexity, but this allows negative tickets whereas removeFromCart doesn't and will revert to 0.
Preview
I need to do some more passes, but it's ready to review.
cc @samselikoff @gaearon @stephan-noel @aurorascharff @brenelz @MaxwellCohen @hernan-yadiel
Goals
the usage examples build up from:
Terms
Edit: I went with
dispatchAction.I struggled with what to call the returned function and the reducer in the signature
I landed on
dispatchActionbecause:dispatchbecause it dispatches likeuseReducerdispatchI landed on
reducerActionbecause:One wierd naming thing is this:
What do you call the argument passed to the action? useReducer calls it an "action", so that would mean it's
So I called it
actionPayload. It's the payload passed to the action.TODO
Followups
useStateanduseTransitiondirectly in useTransition docs.