#Why does my state not update when using useActionState with React?

18 messages · Page 1 of 1 (latest)

snow grotto
ocean sparrowBOT
#

Hi @snow grotto. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

The Astro docs only show the correct wiring; they don’t list all the ways it can go wrong, so I can only answer based on what is explicitly documented.

For useActionState() to update state with Astro Actions, your setup must match the pattern from the React integration guide:

1. The client component must use withState(actions.yourAction)

import { actions } from 'astro:actions';
import { withState } from '@astrojs/react/actions';
import { useActionState } from 'react';

export function Like({ postId }: { postId: string }) {
  const [state, action, pending] = useActionState(
    withState(actions.like),
    { data: 0, error: undefined }, // initial state
  );

  return (
    <form action={action}>
      <input type="hidden" name="postId" value={postId} />
      <button disabled={pending}>{state.data} ❤️</button>
    </form>
  );
}

Here, withState() adapts the Astro Action so React’s useActionState() can track and update state when the form is submitted. If you pass actions.like directly instead of withState(actions.like), React won’t manage the state correctly. [React actions]

2. Your action must be a form-compatible Astro Action

On the server:

import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';

export const server = {
  like: defineAction({
    input: z.object({
      postId: z.string(),
    }),
    handler: async ({ postId }) => {
      // e.g. read current likes from DB and increment
      return /* new like count, e.g. */ 1;
    },
  }),
};

The action must accept FormData (via input schema) and return the new value you expect to see in state.data. [React actions]

#

If your code differs from these patterns (e.g. not using withState, not returning the updated value, or not wiring the form’s action prop to the function from useActionState), React’s state will not change when you click the button.

If you share your current Like component and action code, I can point to the exact mismatch relative to this documented setup.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@snow grotto Kapa.ai is still learning and improving, please let me know how I did by reacting below.

snow grotto
#

@ocean sparrow
Action code:

import { defineAction, type SafeResult } from 'astro:actions';
import { z } from 'astro/zod';
import { getActionState } from '@astrojs/react/actions';

export const server = {
    like: defineAction({
        input: z.object({
        postId: z.string(),
    }),
    handler: async ({ postId }, ctx) => {
        const { data: currentLikes = 0, error } = await getActionState<SafeResult<any, number>>(ctx);

        // handle errors
        if (error) throw error;

        // write to database
        return currentLikes + 1;
    },
    })
};

Like button component code:

import { actions } from 'astro:actions'
import { withState } from '@astrojs/react/actions'
import { useActionState } from 'react';

export function Like({ postId }: { postId: string }) {
  const [state, action, pending] = useActionState(
    withState(actions.like),
    { data: 0, error: undefined }, // initial likes and errors
  );

  return (
    <form action={action}>
      <input type="hidden" name="postId" value={postId} />
      <button disabled={pending}>{state.data} ❤️</button>
    </form>
  );
}

Like component included on page

---
import Layout from '@layouts/Layout.astro'
import { Like } from '@components/Like'
---

<Layout>
    <Like postId="1" />
</Layout>
ocean sparrowBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Your code matches the examples from the docs, so based only on the knowledge sources I can’t see a specific mistake in what you’ve shared. The docs show exactly the same patterns for both the action and the React component:

  • Server action using defineAction, getActionState, and returning currentLikes + 1 in the handler. [React actions]
  • Client component using useActionState(withState(actions.like), { data: 0, error: undefined }) and wiring the returned action to <form action={action}>, then reading state.data in the button. [React actions]

Because your snippets match those examples, the knowledge sources don’t provide any additional documented reasons why the state would not update (for example, they don’t list troubleshooting steps or common pitfalls).

From the docs alone, the only requirements are:

  1. Use withState(actions.like) with useActionState().
  2. Return the new value (e.g. currentLikes + 1) from the action handler.
  3. Use the action function from useActionState as the form’s action prop.
  4. Read the updated value from state.data. [React actions]

You are already doing all of these according to the code you posted.

#

Given that, I don’t have enough information in the docs to explain why your state is not updating. To debug further, you’d need to check things that are not covered in the knowledge sources (e.g. runtime errors, network requests, or how actions is exported from your actions.ts file), which I’m not allowed to speculate about.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@snow grotto Kapa.ai is still learning and improving, please let me know how I did by reacting below.

snow grotto
#

@ocean sparrow If I update my action definition to include accept: 'form' and then also hydrate the like component with client:load or similar, then the state updates as expected.

ocean sparrowBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You’ve essentially discovered the two key requirements that aren’t obvious when just looking at the useActionState example:

  1. The action must accept form data.
    Astro Actions accept JSON by default. When you submit a <form>, the browser sends FormData, so your action needs:

    like: defineAction({
      accept: 'form',
      input: z.object({
        postId: z.string(),
      }),
      handler: async ({ postId }, ctx) => { /* ... */ },
    })
    

    Without accept: 'form', the form submission won’t be parsed as expected and the action won’t receive the data in the shape your input schema describes, so the state won’t update correctly. [Accept form data]

  2. The React component must be hydrated for useActionState to run.
    useActionState and withState() are React client-side features. They only run once the component is hydrated (e.g. client:load, client:visible, etc.). The docs show the React integration pattern but don’t explicitly restate the need for a hydration directive; that’s implied by normal React-in-Astro usage. [React actions]

So your observation matches the documented behavior:

  • Adding accept: 'form' makes the action compatible with form submissions.
  • Hydrating the Like component lets useActionState run on the client and update state.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@snow grotto Kapa.ai is still learning and improving, please let me know how I did by reacting below.

snow grotto
#

@ocean sparrow Why does the like action pass in the postId as a parameter to the handler when its value is never read? If I take the postId out from the parameter, the state doesn't update properly but I don't understand why

ocean sparrowBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The docs don’t explicitly explain why removing postId from the handler parameter breaks state updates, so I can only infer from what is shown.

In the official example, the handler is always written with the full, typed input object:

export const server = {
  like: defineAction({
    input: z.object({
      postId: z.string(),
    }),
    handler: async ({ postId }, ctx) => {
      const { data: currentLikes = 0, error } =
        await getActionState<SafeResult<any, number>>(ctx);

      if (error) throw error;
      return currentLikes + 1;
    },
  }),
};
```[[React actions](<https://docs.astro.build/en/guides/integrations-guide/react/#getactionstate>)]

And on the client, the form always sends `postId`:

```tsx
export function Like({ postId }: { postId: string }) {
  const [state, action, pending] = useActionState(
    withState(actions.like),
    { data: 0, error: undefined },
  );

  return (
    <form action={action}>
      <input type="hidden" name="postId" value={postId} />
      <button disabled={pending}>{state.data} ❤️</button>
    </form>
  );
}
```[[React actions](<https://docs.astro.build/en/guides/integrations-guide/react/#withstate>)]

From the docs we can say:

- The action’s `input` schema declares `postId` as required.  
- The handler signature `({ postId }, ctx)` matches that schema.  
- `withState(actions.like)` and `useActionState()` rely on the action’s types and metadata to manage state. [[React actions](<https://docs.astro.build/en/guides/integrations-guide/react/#withstate>)]

The knowledge sources do not describe what happens if the handler’s first parameter does *not* match the `input` schema (e.g. omitting `postId`), nor do they document why that would break state updates. They only ever show the handler receiving the validated input object, even if the field is not used inside the body.
#

So, all I can say with confidence is:

  • The documented, supported pattern is to keep the handler parameter matching the input object (including postId), even if you don’t read it.
  • The docs do not explain the internal reason why removing postId from the handler parameter causes useActionState not to update.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: