Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started

Contents

On this page

useRpcMutation

Calls a data-changing server/rpc procedure on demand, with submission state, errors and safe retries.

composable Since 1.1.0

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#

meeko
<!-- 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#

UseRpcMutation

ts
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"]>;
}

RpcMutation

ts
interface RpcMutation<Input, Output> {
	mutate(input: Input): Promise<Output>
	readonly pending: ReadonlySignal<boolean>
	readonly error: ReadonlySignal<Error | null>
	readonly data: ReadonlySignal<Output | null>
}

MutationOptions

ts
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
}

RetrySafeMutation

ts
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#

meeko
<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#

meeko
<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#

meeko
<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#

Incorrectts
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.

Correctts
const rename = useRpcMutation("users.rename");

Create it inside a function#

Incorrectts
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.

Correctts
const rename = useRpcMutation("users.rename");
async function save(): Promise<void> { await rename.mutate({ id: "1", name: "Ada" }); }

Input with another type#

Incorrectts
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.

Correctts
const save = () => rename.mutate({ id: String(user.id), name: "Ada" });

An unhandled rejection#

Incorrectmeeko
<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:

Correctmeeko
<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 with pending set to true and error to null in a single update, and ends by writing data or error alongside pending set to false.
  • Security. The call carries the session's CSRF token and Origin; the procedure applies its own guards. With retrySafe, input is saved in browser storage: do not include secrets.
  • retrySafe. While an uncertain intent exists under the key, mutate rejects with MUTATION_UNCERTAIN without sending anything. The server receives the validated key in context.idempotencyKey and handles deduplication.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/en/reference/composables/use-rpc-mutation.md