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

> Learn how to integrate Astryx form components with React 19s useActionState. Pass the dispatch function to changeAction for seamless server-action execution and optimistic UI updates.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-08-04

---

**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`](https://github.com/facebook/astryx/blob/main//packages/core/src/TextInput/TextInput.tsx), Astryx exposes a **`changeAction`** prop with the signature:

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

```tsx
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:

```tsx
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:

```tsx
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`](https://github.com/facebook/astryx/blob/main//packages/core/src/FormLayout/FormLayout.tsx) | Provides `FormLayoutContext` for layout direction |
| `FormLayoutContext` | [`/packages/core/src/FormLayout/FormLayoutContext.ts`](https://github.com/facebook/astryx/blob/main//packages/core/src/FormLayout/FormLayoutContext.ts) | Context exposing `direction` to `Field` |
| `Field` | [`/packages/core/src/Field/Field.tsx`](https://github.com/facebook/astryx/blob/main//packages/core/src/Field/Field.tsx) | Renders label, description, status; propagates ARIA attributes |
| `TextInput` | [`/packages/core/src/TextInput/TextInput.tsx`](https://github.com/facebook/astryx/blob/main//packages/core/src/TextInput/TextInput.tsx) | Consumes `changeAction` and implements optimistic UI |

The flow proceeds: `FormLayout` → `Field` (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

- **Astryx form components are action-agnostic** — they expose `changeAction` for external state management
- **React 19's `useActionState`** provides `[state, dispatch]`; pass `dispatch` directly to `changeAction`
- **Built-in optimistic UI** in Astryx components means no additional configuration for pending states
- **Source files**: [`/packages/core/src/TextInput/TextInput.tsx`](https://github.com/facebook/astryx/blob/main//packages/core/src/TextInput/TextInput.tsx), [`/packages/core/src/Field/Field.tsx`](https://github.com/facebook/astryx/blob/main//packages/core/src/Field/Field.tsx), [`/packages/core/src/FormLayout/FormLayout.tsx`](https://github.com/facebook/astryx/blob/main//packages/core/src/FormLayout/FormLayout.tsx)

## 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`](https://github.com/facebook/astryx/blob/main//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`](https://github.com/facebook/astryx/blob/main//packages/core/src/FormLayout/FormLayout.tsx) support both patterns without modification.