Skip to content

Conversation

@rickhanlonii
Copy link
Member

@rickhanlonii rickhanlonii commented Feb 3, 2026

Preview

  • First commit: claude
  • Second commit: my edits

I need to do some more passes, but it's ready to review.

cc @samselikoff @gaearon @stephan-noel @aurorascharff @brenelz @MaxwellCohen @hernan-yadiel

Goals

  • Client action first (with a mention of form actions / server functions)
  • explain queuing actions (aka, so you can reduce them
  • explain how to "fix" queing (aka optimistic state, or cancelling)
  • sandbox based usage examples

the usage examples build up from:

  • 1 action
  • 2 actions
  • 2 actions with pending states (via action props)
  • 2 actions with pending and optimistic states
  • 2 actions with a

Terms

I struggled with what to call the returned function and the reducer in the signature

const [_, action, _] = useActionState(reducerAction);

I landed on action because:

  • it should use the "action" name, since it's called in a transition, so not just dispatch
  • dispatchAction is too wordy, though that's more what it's doing

I landed on reducerAction because:

  • it has a reducer signature with the first arg
  • it's an "action" so the returned state is updated in a transition
  • it is a reducer inside an action, so it can do side effects

One wierd naming thing is this:

action({type: 'Add'})

What do you call the argument passed to the action? useReducer calls it an "action", so that would mean it's

call action with the action as the only argument.

So I called it update. idk, don't love it.

@github-actions
Copy link

github-actions bot commented Feb 3, 2026

Size changes

Details

📦 Next.js Bundle Analysis for react-dev

This 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});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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) {
Copy link
Contributor

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)?

Copy link
Contributor

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:

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/c0e4c41164885d5d2d40845848b4d64782d7ca61/types/react/index.d.ts#L1958-L1967

I think I like actionPayload better since it kinda follows the pattern of being more explicit of the other parameter names (reducerAction, initialState).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just adding my 2 cents here.

I also first thought maybe payload would do, but then I looked at the Redux docs and that payload is a sibling attribute to type rather than the entire thing and so thought that might be somewhat confusing. In contrast, the useReducer docs don't use the payload convention and have extra properties as direct siblings to type (simpler IMO).

Then I thought, let me look at web standards like those Remix guys. The idea of reducers has some terminology in common with CustomEvent, it has a type that's a string, but then has options and options.detail. Maybe just detail? actionDetail? Not sure it's better than update, but when I read update it do tend to read it more as a verb (and therefore function).

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.
Copy link
Contributor

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.
Copy link
Contributor

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).
Copy link
Contributor

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, not sure if out of scope, but that page is missing the fact that that client references can be passed.

Image


`reducerAction` returns the new state, and triggers a re-render with that state.

#### Caveats {/*reduceraction-caveats*/}
Copy link
Contributor

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* `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

Copy link

@MaxwellCohen MaxwellCohen left a 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?);
Copy link

@MaxwellCohen MaxwellCohen Feb 3, 2026

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) {

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).
Copy link

@MaxwellCohen MaxwellCohen Feb 3, 2026

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 and async code

`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*/}

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.
Copy link
Contributor

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*/}
Copy link
Contributor

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*/}
Copy link
Contributor

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.
Copy link
Contributor

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want this pitfall text because I received the "useOptimistic" version of this message, and the docs didn't mention the warning, prompting me to reach out to @rickhanlonii. My theory is that the lack of documentation about async React is making it harder for developers and LLMS to use these cool new React tools.

The warning text (please include the exact text so that when someone Googles the error in 6 months can find the answer)

An async function with useActionState was called outside of a transition. This is likely not what you intended (for example, isPending will not update correctly). Either call the returned function inside startTransition, or pass it to an `action` or `formAction` prop.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the dispatch function returned is an Action called "action", it's counterintuitive that we need to wrap it again in another startTransition.

Isn't this the way prop callbacks work? If you create a prop is named with action, the component calling the prop needs to wrap it in startTransition. So you have to call anything named action inside a transition, and then inside the function you know it's executed in a transition.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to understand 'action' in the context of this page because we have several concepts that are called action:

  • Components that take a callback that will be run in a transtion is called action or action props, i.e., form component. Forms will pass formdata to the callback.
  • StartTransition takes a callback called action and schedules it to run in a transition. StartTransition will not provide any extra data to the callback.
  • The second return value of useActionState is 'action', which is like an async reducer that can take 0 to 1 values.

The pending state comes from

  • Components - need to expose pending options or use useActionState
  • StartTransition - pending state is exposed in the useTransition hook and not in the StartTransition api function
  • useActionState - pending state is the 3rd value in the array

If you want to add optimistic updates

  • Components have the setOptimistic in the action props callback function.
  • StartTranstion have the setOptimistic in the action callback function.
  • for useActionState's 'action' wrap useActionState's 'action' and setOptimistic in a new function, and pass it to a transition or component.

It seems to me that, in some cases, useActionState's 'action' is the same as components or StartTransition's action, but in several common cases, useActionState's 'action' is part of an action. Thus adding confusion about what is being referenced or the developer's expectations in using useActionState's 'action.'

@samselikoff
Copy link
Contributor

I struggled with what to call the returned function and the reducer in the signature

const [_, action, _] = useActionState(reducerAction);

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?

const [state, asyncDispatch, isPending] = useActionState(asyncReducer, initialState, permalink?);

and

async function yourAsyncReducer(state, action) {
  // await any async functions, then return next state for React to set
}

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.

@samselikoff
Copy link
Contributor

In retrospect, and in light of your recent "Async React" branding (which I think is fantastic), maybe the hook could have been called useAsyncReducer ^_^

@samselikoff
Copy link
Contributor

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>
  );
}

@samselikoff
Copy link
Contributor

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.

@rickhanlonii
Copy link
Member Author

@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 useActionState where the "action" side effect is to call showNotification (a synchronous API)

@samselikoff
Copy link
Contributor

@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 useActionState where the "action" side effect is to call showNotification (a synchronous API)

Yep I mentioned that above, but the fact that they can be async (and normal reducers cannot) seems to be a pretty crucial differentiator!


* `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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you also mention that when used for progressive enhancement with the <form> action prop, it should be FormData? You cover it later in the "Using with <form> action props" section but maybe good to be explicit here as well?

I haven't checked yet but I guess the same would hold for the formAction attributes of button and input mentioned here.


<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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Not sure if it makes sense to also mention the alternative of passing it to an action prop. I know you have an example later down in the "Using with Action props" section, but seems more complete to also mention it here.

2. The <CodeStep step={2}>`action` function</CodeStep> that lets you trigger the `reducerAction`.
3. The <CodeStep step={3}>pending state</CodeStep> that tells you whether `action` is in progress.

To trigger the Action, call the <CodeStep step={2}>`action` function</CodeStep> inside an [Action](/reference/react/useTransition#functions-called-in-starttransition-are-called-actions). React will call your `reducerAction` with the previous state and argument passed to `action`, and return the new <CodeStep step={1}>state</CodeStep>.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

call the action inside of an Action

This feels a bit off, especially since the reason the return value is called action is because it's called inside of a transition. You could also pass action directly to startTransition, as startTransition(action) and that would be acceptable right?


async function addTicket(prevCount) {
return await addToCart(prevCount);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wrapping of addToCart made me wonder if it's just coincidental or if there is some intention behind it? I tried in a separate NextJS project and the result was the same as passing addToCart directly to useActionState.

Goals
Client action first (with a mention of form actions / server functions)

Was the wrapping because of this that you mentioned in the PR description? The terminology of "client action" or why you might need one, isn't used in the doc. May also be good to clarify that client actions will lose progressive enhancement?


<Pitfall>

Aborting an Action isn't always safe, which is why `useActionState` doesn't do it by default.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there some examples of when or why it wouldn't be safe? Otherwise I feel a bit left in the dark.

Comment on lines +164 to +166
startTransition(() => {
action();
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
startTransition(() => {
action();
});
startTransition(action);

Similar to what I mentioned here. Kind of drives the point home that the argument to startTransition is an action (as is part of the result of useActionState), and that wrapping isn't necessary.


async function increment(previousState, formData) {
return previousState + 1;
function reducerAction(state, action) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just adding my 2 cents here.

I also first thought maybe payload would do, but then I looked at the Redux docs and that payload is a sibling attribute to type rather than the entire thing and so thought that might be somewhat confusing. In contrast, the useReducer docs don't use the payload convention and have extra properties as direct siblings to type (simpler IMO).

Then I thought, let me look at web standards like those Remix guys. The idea of reducers has some terminology in common with CustomEvent, it has a type that's a string, but then has options and options.detail. Maybe just detail? actionDetail? Not sure it's better than update, but when I read update it do tend to read it more as a verb (and therefore function).


### My `isPending` flag is not updating {/*ispending-not-updating*/}

If you're calling the action manually (not through a form's `action` prop), make sure you wrap the call in [`startTransition`](/reference/react/startTransition):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can be any action prop, not just a form's.


---

## Troubleshooting {/*troubleshooting*/}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


- **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.
Copy link

@stephan-noel stephan-noel Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants