Skip to content

Server Actions ​

Server-side form processing with createFormAction, the ActionForm component for client-server roundtrips, and the useFormAction hook for programmatic submissions.

Import ​

ts
import {
  createFormAction,
  ActionForm,
  useFormAction,
  parseActionResult,
} from '@ereo/forms'

createFormAction ​

Creates a server-side request handler that parses the request body, validates with an optional schema, and runs a handler.

Signature ​

ts
function createFormAction<T, TResult = unknown>(opts: {
  schema?: ValidationSchema<unknown, T>;
  handler: (values: T) => Promise<TResult>;
  onError?: (error: unknown) => ActionResult<TResult>;
}): (request: Request) => Promise<ActionResult<TResult>>

Parameters ​

NameTypeDescription
schemaValidationSchemaOptional schema for server-side validation
handler(values: T) => Promise<TResult>Business logic -- called with validated values (required)
onError(error: unknown) => ActionResult<TResult>Custom error handler

Returns ​

An async function (request: Request) => Promise<ActionResult<TResult>> that:

  1. Parses the request body (JSON, FormData, or URL-encoded)
  2. Validates against the schema (if provided)
  3. Calls the handler with validated values
  4. Returns { success: true, data } or { success: false, errors }

Example ​

ts
// server: /api/register
import { createFormAction, zodAdapter } from '@ereo/forms'
import { z } from 'zod'

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export const POST = createFormAction({
  schema: zodAdapter(schema),
  handler: async (values) => {
    const user = await db.users.create(values)
    return { id: user.id }
  },
  onError: (error) => ({
    success: false,
    errors: { '': [error instanceof Error ? error.message : 'Server error'] },
  }),
})

ActionForm ​

A <form> component that handles client-side validation, submission to a URL or function, and automatic server error mapping.

Signature ​

ts
function ActionForm<T extends Record<string, any>>(
  props: ActionFormProps<T>
): ReactElement

Props ​

NameTypeDescription
formFormStoreInterface<T>The form store (required)
actionstring | ((values: T) => Promise<ActionResult>)URL endpoint or async function (required)
method'post' | 'put' | 'patch' | 'delete'HTTP method (default 'post')
onSuccess(result: any) => voidCalled with response data on success
onError(errors: Record<string, string[]>) => voidCalled with error map on failure
childrenReactNodeForm content
classNamestringCSS class
idstringForm element ID
encType'application/json' | 'multipart/form-data'Request encoding (default 'application/json')

Behavior ​

  1. Prevents default form submission
  2. Runs client-side validation (form.validate())
  3. If invalid, focuses the first error and announces errors for screen readers
  4. If valid, sends the request (JSON or multipart)
  5. Parses the ActionResult response
  6. Maps server errors back to form fields automatically
  7. Calls onSuccess or onError
  8. Announces submit status for accessibility

Example ​

tsx
import { useForm, useField, ActionForm, required, email } from '@ereo/forms'

function RegisterForm() {
  const form = useForm({
    defaultValues: { email: '', password: '' },
    validators: {
      email: [required(), email()],
      password: [required()],
    },
  })

  const emailField = useField(form, 'email')
  const passwordField = useField(form, 'password')

  return (
    <ActionForm
      form={form}
      action="/api/register"
      onSuccess={(data) => {
        window.location.href = `/welcome/${data.id}`
      }}
      onError={(errors) => {
        console.log('Server errors:', errors)
      }}
    >
      <input {...emailField.inputProps} placeholder="Email" />
      <input {...passwordField.inputProps} type="password" placeholder="Password" />
      <button type="submit">Register</button>
    </ActionForm>
  )
}

With Function Action ​

tsx
<ActionForm
  form={form}
  action={async (values) => {
    const result = await registerUser(values)
    return result // must return ActionResult
  }}
  onSuccess={(data) => router.push('/dashboard')}
>
  {/* fields */}
</ActionForm>

useFormAction ​

Hook for programmatic form submissions without ActionForm.

Signature ​

ts
function useFormAction<T, TResult = unknown>(opts: {
  action: string | ((values: T) => Promise<ActionResult<TResult>>);
  method?: string;
  encType?: 'application/json' | 'multipart/form-data';
}): {
  submit: (values: T) => Promise<ActionResult<TResult>>;
  cancel: () => void;
  isSubmitting: boolean;
  result: ActionResult<TResult> | null;
}

Returns ​

NameTypeDescription
submit(values: T) => Promise<ActionResult<TResult>>Sends values to the action
cancel() => voidAbort the in-flight request
isSubmittingbooleanWhether a request is in progress
resultActionResult<TResult> | nullLast result

Example ​

tsx
function MyForm() {
  const form = useForm({ defaultValues: { name: '' } })
  const { submit, isSubmitting, result } = useFormAction({
    action: '/api/update',
    method: 'PUT',
  })

  const handleSubmit = async () => {
    const valid = await form.validate()
    if (!valid) return

    const response = await submit(form.getValues())
    if (response.success) {
      // handle success
    }
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit() }}>
      {/* fields */}
      <button disabled={isSubmitting}>Save</button>
      {result?.success === false && <div>Error occurred</div>}
    </form>
  )
}

parseActionResult ​

Normalizes various response shapes into a standard ActionResult.

Signature ​

ts
function parseActionResult<T>(response: unknown): ActionResult<T>

Handles these shapes:

InputOutput
{ success: true, data }Passed through
{ success: false, errors }Passed through
{ error: 'msg' }{ success: false, errors: { '': ['msg'] } }
{ errors: { ... } }{ success: false, errors }
{ data: ... }{ success: true, data }
null / undefined{ success: false, errors: { '': ['Empty response'] } }
Other{ success: true, data: response }

ActionResult ​

ts
interface ActionResult<T = unknown> {
  success: boolean;
  data?: T;
  errors?: Record<string, string[]>;
}

When errors is present, keys are dot-notation field paths. An empty string key ('') represents form-level errors not tied to a specific field.

Released under the MIT License.