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:
changeActionaccepts both sync and async functions - Optimistic UI built-in: Astryx uses
useOptimisticinternally 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: 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
changeActionfor external state management - React 19's
useActionStateprovides[state, dispatch]; passdispatchdirectly tochangeAction - Built-in optimistic UI in Astryx components means no additional configuration for pending states
- Source files:
/packages/core/src/TextInput/TextInput.tsx,/packages/core/src/Field/Field.tsx,/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, 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →