Askr
Guides

Forms, Actions, and CRUD

Use forms, actions, and crud with the current published Askr packages.

  • @askrjs/askr0.0.59

How to use forms, actions, and crud

Bind form input in the server action, return field-aware failures without navigation, and redirect only after a successful write.

  1. Import the published @askrjs/askr entrypoint.
  2. Keep forms, actions, and crud configuration next to the component, route, or server composition root that owns it.
  3. Handle pending, unavailable, cancellation, and failure states, then verify the production path.
<form method="post" action="/projects/new">
  <Field><FieldLabel for="name">Name</FieldLabel><Input id="name" name="name" required /></Field>
  <Button type="submit">Create project</Button>
</form>

Goal and architecture

Askr's write path is `defineAction()` plus `ActionForm`, not a generic `onSubmit` handler you wire up by hand. `defineAction({ id, input, invalidates })` declares a named mutation with an `@askrjs/schema` input shape and the query keys it should invalidate on success; `ActionForm` is a native `<form>` bound to that descriptor, described in the source as explicitly not a synthetic event API. Reads go through `createQuery`/`defineQuery` from `@askrjs/askr/data`, so a CRUD screen is really one query for the list, one action per mutation, and the `invalidates` list tying them together.

Implementation

For a form-first flow, render `<ActionForm action={updateUser}>` with normal form fields inside it — no controlled-input plumbing required, since it's a real form submission the framework intercepts. For a programmatic flow (bulk actions, buttons instead of forms), call `action(updateUserAction)` to get back a `state: StateTuple<ActionStatus<TResult>>` and a `submit(input)` function you invoke directly; `ActionStatus` exposes `pending`, `result`, and `error` so you can drive UI off it without extra state. Either way, list the query keys the action should invalidate — that's what makes the table or detail view refresh after a successful mutation without you calling `invalidate()` by hand.

Failure states

A rejected action surfaces an `ActionValidationError` with `fieldErrors: Record<string, string[]>` keyed by field name — render those next to the relevant inputs instead of a single generic error banner. `ActionStatus.error` is unioned as `unknown`, so narrow it before rendering rather than assuming it's always an `Error` instance or always your validation shape. A submit that never resolves — a hung request, a dropped connection — should still leave `pending` truthy so a stuck-forever button doesn't look identical to a successful one.

Verification

Submit a form with missing required fields and confirm `fieldErrors` lines up with the actual field names in your schema, not just a generic failure state. After a successful mutation, confirm every query key listed in `invalidates` actually refetches — a key typo here is the single most common cause of "I saved it but the list didn't update." Then submit the same action twice quickly and confirm you get the behavior you intended, whether that's two writes or a debounced single one; nothing in `action()` serializes concurrent submissions for you.