How to Integrate Astryx Form Components with React 19's useActionState Hook

Astryx form components integrate with React 19's useActionState by passing the hook's dispatch function to the changeAction prop on input components like TextInput, enabling automatic server-action execution with built-in optimistic UI updates.

Astryx provides low-level UI-only form primitives that remain agnostic to state management. When paired with React 19's native useActionState hook, these components become a complete form solution without additional wrappers. This article explains the exact integration pattern using the facebook/astryx source code.

How React 19's useActionState Works

The useActionState hook, introduced in React 19, returns a tuple of [state, dispatch]:

Return value Purpose
state Current state (including optimistic updates)
dispatch Function that invokes the server action

The dispatch function accepts a payload that gets passed to your async server action. Astryx components consume this directly through their changeAction prop.

The changeAction Prop: Astryx's Integration Point

In /packages/core/src/TextInput/TextInput.tsx, Astryx exposes a changeAction prop with the signature:

changeAction?: (value: string) => void | Promise<void>

This matches useActionState's dispatch signature, allowing direct wiring without adapters.

Key Implementation Details

  • Async-first design: changeAction accepts both sync and async functions
  • Optimistic UI built-in: Astryx uses useOptimistic internally to show pending values immediately
  • ARIA preserved: All accessibility attributes (aria-describedby, aria-invalid) remain functional during server actions

Complete Integration Example

import { FormLayout } from '@/packages/core/src/FormLayout/FormLayout';
import { TextInput } from '@/packages/core/src/TextInput/TextInput';
import { useActionState } from 'react';

export function NameForm() {
  // 1️⃣ Initialize server action with React 19's hook
  const [formState, dispatch] = useActionState(
    async (prev, { value }) => {
      // Server-side logic executes here
      await updateUserName(value);
      return { ...prev, name: value };
    },
    { name: '' }  // Initial client-side state
  );

  return (
    <FormLayout direction="vertical">
      <TextInput
        label="Name"
        value={formState.name}
        onChange={() => {}}
        // ✅ Direct connection to useActionState
        changeAction={value => dispatch({ value })}
      />
    </FormLayout>
  );
}

Server-Side Validation Pattern

Astryx components handle errors through the status prop, which pairs naturally with useActionState error states:

import { FormLayout } from '@/packages/core/src/FormLayout/FormLayout';
import { TextInput } from '@/packages/core/src/TextInput/TextInput';
import { useActionState } from 'react';

export function ValidatedNameForm() {
  const [state, dispatch] = useActionState(
    async (prev, { value }) => {
      if (value.length < 3) {
        throw new Error('Name must be at least 3 characters');
      }
      await saveNameToDB(value);
      return { ...prev, name: value, error: undefined };
    },
    { name: '', error: undefined }
  );

  return (
    <FormLayout direction="vertical">
      <TextInput
        label="Name"
        value={state.name}
        onChange={() => {}}
        changeAction={value => dispatch({ value })}
        status={state.error 
          ? { type: 'error', message: state.error } 
          : undefined
        }
      />
    </FormLayout>
  );
}

Multi-Field Forms with Horizontal Layout

For complex forms, combine FormLayout with multiple inputs dispatching differentiated actions:

import { FormLayout } from '@/packages/core/src/FormLayout/FormLayout';
import { TextInput } from '@/packages/core/src/TextInput/TextInput';
import { useActionState } from 'react';

export function AddressForm() {
  const [addr, dispatch] = useActionState(
    async (prev, { field, value }) => {
      await updateAddressField(field, value);
      return { ...prev, [field]: value };
    },
    { street: '', city: '' }
  );

  return (
    <FormLayout direction="horizontal">
      <TextInput
        label="Street"
        value={addr.street}
        onChange={() => {}}
        changeAction={v => dispatch({ field: 'street', value: v })}
      />
      <TextInput
        label="City"
        value={addr.city}
        onChange={() => {}}
        changeAction={v => dispatch({ field: 'city', value: v })}
      />
    </FormLayout>
  );
}

Architecture: How the Pieces Connect

Component File Path Role in Integration
FormLayout /packages/core/src/FormLayout/FormLayout.tsx Provides FormLayoutContext for layout direction
FormLayoutContext /packages/core/src/FormLayout/FormLayoutContext.ts Context exposing direction to Field
Field /packages/core/src/Field/Field.tsx Renders label, description, status; propagates ARIA attributes
TextInput /packages/core/src/TextInput/TextInput.tsx Consumes changeAction and implements optimistic UI

The flow proceeds: FormLayoutField (via context) → TextInput (receives changeAction). When dispatch from useActionState connects to changeAction, the integration completes.

Performance Characteristics

Astryx's integration with useActionState maintains these behaviors:

  • Optimistic by default: Inputs show new values immediately, rolling back on failure
  • No wrapper overhead: Direct prop passing eliminates HOC or render-prop patterns
  • Server-component compatible: ARIA attributes work in React Server Components
  • Batched dispatches: Multiple rapid changes are automatically batched by React 19

Summary

Frequently Asked Questions

Does Astryx require any wrapper components for useActionState?

No. According to the facebook/astryx source code, Astryx components accept changeAction directly. Pass the dispatch function from useActionState as-is—no adapter or wrapper needed. The type signatures align exactly: both accept (value) => void | Promise<void>.

What happens to the UI while the server action is running?

Astryx components show the optimistic state immediately. In /packages/core/src/TextInput/TextInput.tsx, the component uses useOptimistic internally to reflect user input right away. If the server action fails, React 19 automatically reverts to the previous state.

Can I use useActionState with Astryx in React 18?

useActionState is a React 19 feature. In React 18, use the experimental useTransition with manual optimistic state, or upgrade to React 19 where useActionState is stable. Astryx's changeAction prop works with any async function signature, so alternative patterns remain compatible.

How do I handle form-level submission vs. per-field changes?

Astryx's architecture separates these concerns. Use changeAction for per-field server updates (as shown above), or omit it and use a traditional form action prop for submission-time validation. The components in /packages/core/src/FormLayout/FormLayout.tsx support both patterns without modification.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →