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
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! |
|
|
||
| - **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 |
|
@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).
|
|
Ok I think this is good to go if someone want's to stamp it |
|
My first reaction on re-reading was that |
|
Yeah I agree @samselikoff, but the convention is that Action is a suffix |
|
But only on the public API side, right? Like if I define or expose an |
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.