useRpcMutation
Calls a data-changing server/rpc procedure on demand, with submission state, errors and safe retries.
useRpcMutation(procedimiento, opciones?) prepares a call to a data-changing server/rpc procedure. It does nothing until you call mutate(input), never in SSR; it then sends the CSRF token like rpc.*, updates pending, data and error and returns the procedure's output, or rejects with its error. It is the write counterpart to useRpcQuery, and the RPC counterpart to useApiMutation.
It is generated by the application's clients and auto-imported in .meeko files; call it at <script> top level.
Usage#
<!-- app/components/RenameUser.meeko -->
<script lang="ts">
const props = defineProps<{ id: string; name: string }>();
const rename = useRpcMutation("users.rename");
const name = signal(props.name);
async function save(): Promise<void> {
await rename.mutate({ id: props.id, name: name.value });
}
</script>
<template>
<input model="name">
<button type="button" bind:disabled="rename.pending.value" on:click="save">Save</button>
<p if:when="rename.error.value" role="alert">{rename.error.value.message}</p>
</template>Type#
type UseRpcMutation<Contract extends Record<string, RpcOperation>> = {
<Procedure extends keyof Contract & string>(
procedure: Procedure,
options?: MutationOptions<
Contract[Procedure]["input"],
Contract[Procedure]["output"]
> & { readonly retrySafe?: undefined },
): RpcMutation<Contract[Procedure]["input"], Contract[Procedure]["output"]>;
<Procedure extends keyof Contract & string>(
procedure: Procedure,
options: MutationOptions<
Contract[Procedure]["input"],
Contract[Procedure]["output"]
> & { readonly retrySafe: RetrySafeOptions },
): RpcMutation<Contract[Procedure]["input"], Contract[Procedure]["output"]> &
RetrySafeMutation<Contract[Procedure]["input"], Contract[Procedure]["output"]>;
}interface RpcMutation<Input, Output> {
mutate(input: Input): Promise<Output>
readonly pending: ReadonlySignal<boolean>
readonly error: ReadonlySignal<Error | null>
readonly data: ReadonlySignal<Output | null>
}interface MutationOptions<Input = unknown, Output = unknown> {
readonly retrySafe?: RetrySafeOptions
readonly onSuccess?: (output: Output, attempt: MutationAttempt<Input>) => unknown
readonly onError?: (error: Error, attempt: MutationFailure<Input>) => unknown
}interface RetrySafeMutation<Input, Output> {
readonly uncertain: ReadonlySignal<UncertainMutation<Input> | null>
retry(): Promise<Output | undefined>
discard(): void
}Parameters#
| Parameter | Type | Default | Description |
|---|---|---|---|
procedimiento |
procedure ID | required | Such as "users.rename". |
opciones.retrySafe |
{ key, storage? } |
none | Since 1.2. Saves the intent with an idempotency key and retries it with the same key if there is no response. |
opciones.onSuccess |
(output, attempt) => unknown |
none | Since 1.2. Once per successful attempt, including automatic retries. |
opciones.onError |
(error, attempt) => unknown |
none | Since 1.2. Once per failed attempt; attempt.uncertain indicates no response was received. |
Return value#
An RpcMutation<Input, Output>:
| Property | Type | Description |
|---|---|---|
mutate(input) |
Promise<Output> |
Calls the procedure. Resolves with its output or rejects with its error (a ClientError if the server responded). |
pending |
ReadonlySignal<boolean> |
true while a call is in progress. |
data |
ReadonlySignal<Output | null> |
Output of the last successful call, or null. |
error |
ReadonlySignal<Error | null> |
Failure of the last call, or null. |
With retrySafe, adds uncertain, retry() and discard() (RetrySafeMutation).
Examples#
Write and read again#
<script lang="ts">
const props = defineProps<{ params: { id: string }; search: Record<string, string | string[]> }>();
const user = await useApi("/api/users/[id]", { params: { id: props.params.id } });
const rename = useRpcMutation("users.rename");
async function save(name: string): Promise<void> {
await rename.mutate({ id: props.params.id, name });
await user.refresh();
}
</script>Notifications for each attempt#
<script lang="ts">
const rename = useRpcMutation("users.rename", {
onSuccess: (user) => notify(`Saved como ${user.name}.`),
onError: (error) => notify(isClientError(error) ? "The name is invalid." : "Could not connect."),
});
</script>onSuccess and onError are called after updating data, error and pending. The mutate promise still resolves or rejects as usual.
A payment safe against retries#
<script lang="ts">
const charge = useRpcMutation("sales.checkout", { retrySafe: { key: "pos:sale" } });
const failure = signal("");
async function pay(input: SaleInput): Promise<void> {
failure.value = "";
try {
await charge.mutate(input);
} catch (error) {
// No response: remains uncertain and will retry with the same key.
if (!charge.uncertain.value) failure.value = error instanceof Error ? error.message : String(error);
}
}
function discard(): void {
if (window.confirm("Discard this sale only if you have confirmed it was not recorded.")) charge.discard();
}
</script>
<template>
<div role="alert" if:when="charge.uncertain.value">
<p>Sale pending verification: it will retry with the same key, so it will not charge twice.</p>
<button bind:disabled="charge.pending.value" on:click="() => charge.retry().catch(() => {})">Retry now</button>
<button on:click="discard">Discard</button>
</div>
<p if:when="failure.value">{failure.value}</p>
</template>Common mistakes#
Await it with await#
const rename = await useRpcMutation("users.rename");meeko check reports MEEK055: useRpcMutation no se puede esperar con await; llámalo sin await y espera su mutate(input) en un handler.
const rename = useRpcMutation("users.rename");Create it inside a function#
async function save(): Promise<void> {
await useRpcMutation("users.rename").mutate({ id: "1", name: "Ada" });
}meeko check reports MEEK054: useRpcMutation() debe llamarse en el nivel superior de <script>, no dentro de una función, un handler, un effect ni onMounted.
const rename = useRpcMutation("users.rename");
async function save(): Promise<void> { await rename.mutate({ id: "1", name: "Ada" }); }Input with another type#
const save = () => rename.mutate({ id: 1, name: "Ada" });meeko check reports TS2322: Type 'number' is not assignable to type 'string': input is checked against the procedure's input schema.
const save = () => rename.mutate({ id: String(user.id), name: "Ada" });An unhandled rejection#
<button on:click="() => rename.mutate({ id, name: name.value })">Save</button>If the server responds with an error, the promise rejects; the handler delivers it to the nearest provideErrorHandler (source "event") and rename.error reflects it. To handle the error there, catch it:
<button on:click="() => rename.mutate({ id, name: name.value }).catch(() => {})">Save</button>
<p if:when="rename.error.value" role="alert">Could not save.</p>Notes#
- Never in SSR. The server and navigation dry runs never send a mutation; with
retrySafe, they do not retry it either. - Consistent state. Since
1.2, each submission starts withpendingset totrueanderrortonullin a single update, and ends by writingdataorerroralongsidependingset tofalse. - Security. The call carries the session's CSRF token and
Origin; the procedure applies its own guards. WithretrySafe, input is saved in browser storage: do not include secrets. retrySafe. While an uncertain intent exists under the key,mutaterejects withMUTATION_UNCERTAINwithout sending anything. The server receives the validated key incontext.idempotencyKeyand handles deduplication.