withastro/astro · error · AstroError

`getActionState()` could not find a state object.

Error message

`getActionState()` could not find a state object.

What it means

After the form Content-Type check passes, getActionState() looks for the _astroActionState form field that React's useActionState adds when the action is wrapped with the adapter's withState() helper. If the field is absent, the form was submitted without that wrapper, so there is no state payload to deserialize.

Solutions

  1. Wrap the action before passing it to useActionState: useActionState(withState(action), initialState)
  2. Read the state in the handler via getActionState({ request }) only in actions that are used with useActionState
  3. Do not call getActionState from actions that plain forms submit directly

Example fix

// before
const [state, formAction] = useActionState(myAction, null);
// after
import { withState } from '@astrojs/react/actions';
const [state, formAction] = useActionState(withState(myAction), null);
Defensive patterns

Strategy: validation

Validate before calling

const fd = await request.clone().formData();
if (!fd.has('_astroActionState')) {
  // Submitted without withState()/useActionState; nothing to read
  return null;
}
const state = await getActionState({ request });

Type guard

async function hasActionState(request: Request): Promise<boolean> {
  const fd = await request.clone().formData();
  return typeof fd.get('_astroActionState') === 'string';
}

Try / catch

let state: T | null = null;
try {
  state = await getActionState<T>({ request });
} catch (err) {
  if (String(err?.message).includes('could not find a state object')) {
    state = null; // form posted without withState wrapper
  } else throw err;
}

Prevention

When it happens

Trigger: Passing the raw action to useActionState instead of withState(action); submitting the action through a plain <form action={action}> with no useActionState; a hand-crafted POST that omits the hidden _astroActionState field.

Common situations: Adopting getActionState on an existing form flow that never used useActionState; forgetting that withState is what injects the serialized state into the payload; React 18 setups where useActionState is unavailable.

Related errors


AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18). Data as JSON: /api/errors/2875fe82b4211a28. Report an issue: GitHub.

Appendix: source

Thrown at packages/integrations/react/src/actions.ts:58

	return callback;
}

/**
 * Retrieve the state object from your action handler when using `useActionState()`.
 * To ensure this state is retrievable, use the {@linkcode withState} helper.
 */
export async function getActionState<T>({ request }: { request: Request }): Promise<T> {
	const contentType = request.headers.get('Content-Type');
	if (!contentType || !isFormRequest(contentType)) {
		throw new AstroError(
			'`getActionState()` must be called with a form request.',
			"Ensure your action uses the `accept: 'form'` option.",
		);
	}
	const formData = await request.clone().formData();
	const state = formData.get('_astroActionState')?.toString();
	if (!state) {
		throw new AstroError(
			'`getActionState()` could not find a state object.',
			'Ensure your action was passed to `useActionState()` with the `withState()` wrapper.',
		);
	}
	return JSON.parse(state) as T;
}

const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];

function isFormRequest(contentType: string) {
	// Split off parameters like charset or boundary
	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type#content-type_in_html_forms
	const type = contentType.split(';')[0].toLowerCase();

	return formContentTypes.some((t) => type === t);
}

/**

View on GitHub (pinned to 52e6c34790)