Browser clients and mutations
Call typed application APIs and handle user-initiated writes consistently.
Call application JSON APIs through a typed Hono client.
Wrap user-initiated async work in mutation.create() so loading, errors,
aborts, retries, and stale results follow one contract.
Create a typed client
Export the Hono route type from the application server:
export type InventoryApi = typeof inventoryRoutes;Create the browser client in a browser-safe module:
import { api } from "@k2b/cloud/browser";
import type { InventoryApi } from "../api";
export const inventoryApi = api.create<InventoryApi>({
baseUrl: "/api/inventory",
});The client infers route parameters and request payloads. Check response.ok
before reading success data.
On a page of the mobile app, the client
also keeps the app session alive. When a request answers 401, the client
renews the app session through Core and, once the phone has a valid session,
sends the request once more. A 401 means that the server did not apply the
request: the authentication middleware answers it before any route handler
runs, and a route that answers 401 itself must do so before it changes
anything, as described in
Return expected failures.
The repeat therefore applies a change at most once. When the phone is no
longer paired, the app opens its pairing view instead. On the web, a 401 is
returned as it is.
Do not use raw fetch() for an application JSON API when its typed route is
available.
Run a mutation
import { mutation } from "@k2b/stdlib/solid";
import { toast } from "@k2b/ui";
const archive = mutation.create<void, { itemId: string }>({
mutation: async ({ itemId }, { abortSignal }) => {
const response = await inventoryApi.items[":id"].$delete(
{ param: { id: itemId } },
{ init: { signal: abortSignal } },
);
if (!response.ok) throw new Error("Item could not be archived.");
},
onSuccess: () => toast.success("Item archived"),
onError: (error) => toast.error(error.message),
});mutate(vars) starts the operation. loading(), error(), and data() are
reactive accessors.
abort() cancels the active operation. An aborted fetch calls onAbort, not
onError.
retry() repeats the previous variables and context. It does not run
onBefore again.
When a newer mutation starts, a late result from an older mutation is ignored.
Capture the complete retryable intent before the request starts. Mutation variables or one-time context must include selected resources, destinations, the request payload, idempotency keys, and correlation IDs. A retry must not read a new choice from mutable UI state or reuse an idempotency key with a different payload.
onSuccess, onError, onAbort, and onFinally are synchronous hooks. Their
return values are not awaited. Put work that defines the command outcome in the
mutation function. Track post-write reconciliation separately.
error() and onError report only a failed mutation function. onSuccess
runs after data() and loading() are updated; an error thrown there, or by a
component that renders the result, does not count as a failed write. A render
error reaches the island's boundary
(When an island fails),
and mutate() and retry() reject with the remaining ones. Await mutate()
in try/catch only when the caller must react to such an error.
Add optimistic state carefully
Every action answers at once, with an optimistic update or a pending state on the acted element. Show mutation state decides which one an action gets and when it offers Undo. This section covers the mechanics.
onBefore may return context used by success, error, abort, and finally hooks.
Use it to capture the previous UI state, such as a removed row and its
position, before the optimistic change.
Restore that state on error and abort, in the same place, so the item returns where the person left it. Restore it only if no later action on the same item has taken over that state, so an earlier failure never rolls back a newer choice; then reconcile with the server. Do not optimistically grant permission, expose new data, or pretend an irreversible action completed.
For an action that offers Undo, Undo either cancels a request that has not been sent yet or sends a request that reverses the change on the server. Cancelling an unsent request never makes a hard delete eligible for Undo. Undo restores the captured state at once, so keep that context until the Undo notice closes. Treat Undo as its own mutation: reconcile with the server after it, and if the reversal is refused or fails, show the state the server kept and an error with the reason.
The server remains authoritative. Reconcile the returned resource or reload the affected server-backed view after success. If that refresh fails, keep the optimistic result; the write already succeeded.
Separate query and mutation state
Use a mutation for a user-initiated write or command. Do not use a mutation to load a server-backed result set.
Start result sets with URL-addressed SSR data and keep them current with an owner-local query. See Server-backed state.
Do not turn the mutation result into a client-side cache of the application's domain model.
See Forms, prompts, and feedback for presenting the operation.