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

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

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

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

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.

5 participants