-
Notifications
You must be signed in to change notification settings - Fork 7.9k
[wip] Rewrite useActionState #8284
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
base: main
Are you sure you want to change the base?
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! 🙌 |
| </form> | ||
| ) | ||
| function MyComponent() { | ||
| const [state, action, isPending] = useActionState(reducerAction, {quantity: 1}); |
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.
| const [state, action, isPending] = useActionState(reducerAction, {quantity: 1}); | |
| const [state, action, isPending] = useActionState(reducerAction, initialState); |
so it matches what the reader is reading below.
|
|
||
| async function increment(previousState, formData) { | ||
| return previousState + 1; | ||
| function reducerAction(state, action) { |
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.
Prob. a typo.
Below you used update, and as you mentioned on the PR descrition, you are not convinced, me neither. update sounds more like a function name. What do you think about actionPayload (or payload)?
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.
also, I noticed that in @types/react it is named payload:
I think I like actionPayload better since it kinda follows the pattern of being more explicit of the other parameter names (reducerAction, initialState).
| 3. The `isPending` flag that tells you whether there is a pending Transition. | ||
| 1. The current state. During the first render, it will match the `initialState` you passed. After the action is invoked, it will match the value returned by the `reducerAction`. | ||
| 2. An `action` function that you call inside [Actions](/reference/react/useTransition#functions-called-in-starttransition-are-called-actions). | ||
| 3. The `isPending` flag that tells you whether there is a pending Action. |
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 think this could read a little "global", like "any Action anywhere". My understanding is that isPending is scoped to the specific action returned by that useActionState call. Maybe something like "whether this action is pending" or "whether the action returned by this hook call is pending" would be a bit harder to misread.
| * `useActionState` is a Hook, so it must be called **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 `action` sequentially, allowing each `reducerAction` to use the result of the previous Action. | ||
| * The `action` function has a stable identity, so you will often see it omitted from Effect dependencies, but including it will not cause the Effect to fire. If the linter lets you omit a dependency without errors, it is safe to do. [Learn more about removing Effect dependencies.](/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect) | ||
| * When using the `permalink` option, ensure the same form component is rendered on the destination page (including the same `reducerAction` and `permalink`) so React knows how to pass the state through. Once the page becomes interactive, this parameter has no effect. |
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.
Is there an example where permalink is used outside of the <form action={...}> case?
I wasn’t totally sure if permalink is strictly a form/progressive-enhancement thing, or if it can apply more broadly. If it’s not form-only, I was leaning toward dropping "form" in "ensure the same form component is rendered…"
| * React queues and executes multiple calls to `action` sequentially, allowing each `reducerAction` to use the result of the previous Action. | ||
| * The `action` function has a stable identity, so you will often see it omitted from Effect dependencies, but including it will not cause the Effect to fire. If the linter lets you omit a dependency without errors, it is safe to do. [Learn more about removing Effect dependencies.](/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect) | ||
| * When using the `permalink` option, ensure the same form component is rendered on the destination page (including the same `reducerAction` and `permalink`) so React knows how to pass the state through. Once the page becomes interactive, this parameter has no effect. | ||
| * When using Server Functions, `initialState` needs to be serializable (values like plain objects, arrays, strings, and numbers). |
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 think it could be helpful to link to the page that spells out what’s serializable and what's not:
serializable parameters and return values
| `reducerAction` returns the new state, and triggers a re-render with that state. | ||
| #### Caveats {/*reduceraction-caveats*/} |
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.
One thing I personally struggled with the first time (and many times) I used this hook with TypeScript is that reducerAction return type must match the type of initialState.
Would it make sense to add a short note in this caveats section about keeping the return type consistent with initialState? I think it would save people some head-scratching🫣
| * `previousState`: The current state of the Action. Initially this is equal to the `initialState`. After the first call to `action`, it's equal to the last state returned. | ||
| * `update`: The argument passed to `action`. 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * `update`: The argument passed to `action`. 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. | |
| * **optional** `update`: The argument passed to `action`. 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. |
Never used it without this second argument, but it makes total sense
MaxwellCohen
left a comment
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.
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
|
|
||
| ```js | ||
| const [state, formAction, isPending] = useActionState(fn, initialState, permalink?); | ||
| const [state, action, isPending] = useActionState(reducerAction, 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.
The term action here is a little vague because reducers take an action, and it is called in Action; it seems like React is overloading the term action. Since 'useActionState' is like useReducer + Actions + side effects, using the useReducer reducer/dispatch language might be clearer.
ie
const [state, actionDispatch, isPending] = useActionState(actionReducer, initialState, permalink?)|
|
||
| async function increment(previousState, formData) { | ||
| return previousState + 1; | ||
| function reducerAction(state, action) { |
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.
Should the async be removed from reducerAction?
| <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 track the state of an [Action](/reference/react/useTransition#functions-called-in-starttransition-are-called-actions). |
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.
This line feels a little vague in explaining what makes useActionState special compared to other hooks. UseActionState allows us to have state updates with side effects.
`useActionState` is a React Hook that manages state updates with side effects specifically within [Action](/reference/react/useTransition#functions-called-in-starttransition-are-called-actions)
| </DeepDive> | ||
| ### Using multiple Action types {/*using-multiple-action-types*/} |
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.
Is this the reducer patern, if so should it be in the title?
| - **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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
| --- | ||
| ### Using with `<form>` action props {/*use-with-a-form*/} |
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 think it would be really helpful to show how it can be used for error ui. The troubleshooting section hints at “return error state instead of throwing”, but I could see people missing how to structure it.
Maybe we could add a small example where initialState is something like { error: null, count: 0 }, and the reducer returns { error: '...' } when the server call fails.
| --- | ||
| ## Usage {/*usage*/} |
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've seen a pattern in a few places where people use .bind to "pre-fill" an argument before passing the reducer function to useActionState. Something like:
function updateCart(userId, prevState, payload) {
// ...
}
function Checkout({ userId }) {
const updateCartForUser = updateCart.bind(null, userId);
const [state, action] = useActionState(updateCartForUser, { error: null });
// ..
}Is this pattern encouraged?
I can see how it can get a bit weird with TypeScript.
| <Pitfall> | ||
| When calling the `action` function, you must wrap the call in [`startTransition`](/reference/react/startTransition). If you call `action` without `startTransition`, the `isPending` flag will not update correctly, and React will show a warning in development. |
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.
What is the reason behind this pitfall? If the dispatch function returned is an Action called "action", it's counterintuitive that we need to wrap it again in another startTransition. As @MaxwellCohen said above, something like [state, actionDispatch, isPending] = useActionState(actionReducer, initialState, permalink?) would make more sense since we still need to either wrap the dispatch in useTransitionor pass it to a form action or other action prop.
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
I struggled with what to call the returned function and the reducer in the signature
I landed on
actionbecause:dispatchdispatchActionis too wordy, though that's more what it's doingI 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
update. idk, don't love it.