JSPM

actfields

0.1.0
    • ESM via JSPM
    • ES Module Entrypoint
    • Export Map
    • Keywords
    • License
    • Repository URL
    • TypeScript Types
    • README
    • Created
    • Published
    • Downloads 8
    • Score
      100M100P100Q60317F
    • License MIT

    Field-level helpers for React 19 Actions and useActionState.

    Package Exports

    • actfields

    Readme

    actfields

    Field-level helpers for React 19 Actions and useActionState.

    Why

    React 19's useActionState gives you form-level state after a server action completes. What it doesn't give you is per-field ergonomics: which field has an error, what value should be shown after a failed submit, which ARIA attributes need to be set.

    react-hook-form solves this but brings a controlled-input model, a schema abstraction layer, and 24 kB min+gz. If you're just wiring a form to a React 19 Action, that's overkill.

    actfields is the missing middle: a ~1 kB wrapper that turns your action's return value into ready-to-spread field props.

    Install

    npm install actfields
    # react >= 19 required as a peer dep

    Quickstart with Zod

    app/actions.ts (server component or 'use server' file)

    'use server'
    import { z } from 'zod'
    import { actionResult } from 'actfields'
    
    const schema = z.object({
      email: z.string().email(),
      password: z.string().min(8),
    })
    
    export async function loginAction(prev, formData) {
      const parsed = schema.safeParse(Object.fromEntries(formData))
      if (!parsed.success) {
        return actionResult(
          Object.fromEntries(formData),
          parsed.error.flatten().fieldErrors,
        )
      }
      await authenticate(parsed.data)
      return actionResult(parsed.data)
    }

    app/LoginForm.tsx (client component)

    'use client'
    import { useActionFields } from 'actfields'
    import { loginAction } from './actions'
    
    export function LoginForm() {
      const { state, formAction, pending, fields } = useActionFields(loginAction, {
        values: {},
        errors: {},
      })
    
      return (
        <form action={formAction}>
          <input type="email" {...fields.email} />
          {fields.email.error && (
            <p id={fields.email['aria-describedby']}>{fields.email.error}</p>
          )}
    
          <input type="password" {...fields.password} />
          {fields.password.error && (
            <p id={fields.password['aria-describedby']}>{fields.password.error}</p>
          )}
    
          {state.formError && <p role="alert">{state.formError}</p>}
    
          <button disabled={pending}>{pending ? 'Signing in…' : 'Sign in'}</button>
        </form>
      )
    }

    API Reference

    useActionFields(action, initial)

    React hook. Client-side only.

    function useActionFields<T>(
      action: (prev: ActionState<T>, formData: FormData) => Promise<ActionState<T>>,
      initial: ActionState<T>,
    ): {
      state: ActionState<T>
      formAction: (formData: FormData) => void
      pending: boolean
      fields: { [K in keyof T]: FieldHelpers }
    }
    • state — the current ActionState, same shape as what your action returns.
    • formAction — pass directly to <form action={formAction}>.
    • pendingtrue while the action is in flight.
    • fields — a proxy object. Access fields.myField to get helpers for that field:
      • name — the field name string (for uncontrolled inputs).
      • defaultValue — the value from state.values (preserves input after failed submit).
      • error — first error string, or undefined.
      • aria-invalidtrue when an error is present, absent otherwise.
      • aria-describedby"${name}-error" when an error is present, absent otherwise.

    actionResult(values, errors?)

    Server-side (or client-side) helper. Shapes the return value so types align.

    function actionResult<T>(
      values: Partial<T>,
      errors?: FieldErrors<T>,
    ): ActionState<T>

    Types

    type FieldErrors<T> = Partial<Record<keyof T, string | string[]>>
    
    type ActionState<T> = {
      values: Partial<T>
      errors: FieldErrors<T>
      formError?: string
    }
    
    type FieldHelpers = {
      name: string
      defaultValue: string | number | undefined
      error?: string
      'aria-invalid'?: true
      'aria-describedby'?: string
    }

    Recipes

    Access the full error array

    fields.email.error returns the first string. For all errors, read state.errors.email directly:

    {Array.isArray(state.errors.email) &&
      state.errors.email.map((e) => <p key={e}>{e}</p>)}

    Form-level error

    Return formError from your action:

    // in your action
    return { values: {}, errors: {}, formError: 'Invalid credentials' }

    Then render it:

    {state.formError && <p role="alert">{state.formError}</p>}

    Async validation (e.g. check username availability)

    Run the check inside your action before returning actionResult. The lib doesn't care — it just renders what the action returns.

    Optimistic UI

    Use React 19's useOptimistic alongside useActionFields. They compose naturally since formAction is just a function.

    Works with Next.js?

    Yes. Pass Server Actions directly to useActionFields. The action parameter type matches Next.js's Server Action signature.

    Works without a framework?

    Yes. Point action at any async function that takes (prevState, formData) and returns ActionState<T>. Works in Vite, CRA, or bare React.

    License

    MIT