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

useRpcQuery

Reads a server/rpc procedure from a component, with SSR, hydration, reactive input reloading and contract types.

composable Since 1.1.0

useRpcQuery(procedimiento, input?, recarga?) is useApi for server/rpc procedures: it calls the procedure with input and returns the same resource, with data, error, pending, refreshing and refresh. Name the procedure by its dotted ID (server/rpc/users/activity.ts is "users.activity") and TypeScript checks input and the shape of data against its contract.

With await, SSR waits for data and hydration reads it from the payload; without await, the request starts in the browser on mount. input can contain signals or be a function reading them: a change requests the data again. Application clients generate it and it is auto-imported in .meeko files; call it only at the top level of <script>.

Usage#

meeko
<!-- app/pages/users/[id]/activity.meeko -->
<script lang="ts">
const props = defineProps<{ params: { id: string }; search: Record<string, string | string[]> }>();
const tab = signal("all");
const activity = await useRpcQuery("users.activity", () => ({ id: props.params.id, tab: tab.value }));
</script>

<template>
  <button type="button" on:click="() => tab.value = 'comments'">Comments</button>
  <p>{activity.data.value.length} events</p>
</template>

Changing tab calls the procedure again; meanwhile, the previous list remains on screen.

Type#

UseRpcQuery

ts
type UseRpcQuery<Contract extends Record<string, RpcOperation>> = <
  Procedure extends keyof Contract & string,
>(
  procedure: Procedure,
  ...input: RpcQueryArguments<Contract[Procedure]["input"]>
) => DataResource<Contract[Procedure]["output"]>

DataResource

ts
interface DataResource<T> extends PromiseLike<LoadedResource<T>> {
	readonly data: ReadonlySignal<T | null>
	readonly error: ReadonlySignal<Error | null>
	readonly pending: ReadonlySignal<boolean>
	readonly refreshing: ReadonlySignal<boolean>
	refresh(): Promise<void>
}

DataResourceOptions

ts
interface DataResourceOptions {
	readonly keepPrevious?: boolean
	readonly debounceMs?: number
	readonly enabled?: boolean | ReadonlySignal<boolean> | (() => boolean)
}

Parameters#

Parameter Type Default Description
procedimiento a procedure ID required Such as "users.activity". The editor completes application procedures.
input procedure input, with fields that can be signals, or () => input {} if accepted by the schema Validated against the procedure's input schema. If the schema accepts {}, you can omit it and {} is sent.
recarga.keepPrevious boolean true Since 1.5. With false, an input change clears data.
recarga.debounceMs number 0 Since 1.7. Waits for input to settle before requesting; does not delay initial loading, refresh() or SSR.
recarga.enabled boolean, signal or () => boolean true Since 1.7. While false, no request is made and any in-flight request is canceled. A resource with await cannot start disabled.

Return value#

A DataResource<T>, where T is the procedure's output; with await, a LoadedResource<T> whose data is never null. Its properties are those of useApi: data, error, pending, refreshing and refresh().

Examples#

Search as you type (since 1.7)#

meeko
<script lang="ts">
const query = signal("");
const results = useRpcQuery("docs.search", () => ({ q: query.value.trim() }), {
  debounceMs: 200,
  enabled: () => query.value.trim().length >= 2,
});
</script>

<template>
  <input type="search" model="query" aria-controls="results">
  <ul id="results" bind:aria-busy="results.pending.value ? 'true' : undefined">
    <li each:of="results.data.value?.hits ?? []" each:as="hit" each:key="hit.url"><a bind:href="hit.url">{hit.title}</a></li>
  </ul>
</template>

Each change cancels the in-flight request; with fewer than two letters, nothing is requested.

A summary without input#

ts
// In the <script> of app/components/Stats.meeko
const stats = await useRpcQuery("stats.summary"); // the schema accepts {}: sends {}

Multiple resources in one region#

meeko
<script lang="ts">
const users = useRpcQuery("users.list");
const totals = useRpcQuery("stats.summary");
</script>

<template>
  <MeekoAwait for="[users, totals]" reserve>
    <snippet name="pending"><p>Loading…</p></snippet>
    <p>{users.data.value.length} users, {totals.data.value.orders} orders</p>
  </MeekoAwait>
</template>

<MeekoAwait> renders its content when both have data, and inside, meeko check reads data as loaded.

Reactive fields in input#

Instead of a function, each input field can be a signal:

meeko
<script lang="ts">
const page = signal(1);
const list = useRpcQuery("users.page", { page });
</script>

<template><button type="button" on:click="() => page.value++">More</button></template>

Common mistakes#

A procedure that does not exist#

Incorrectts
const activity = useRpcQuery("users.activiti", { id: "1", tab: "a" });

meeko check reports TS2345: Argument of type '"users.activiti"' is not assignable to parameter of type '"docs.search" | "users.activity" | "users.rename"', with the list of procedures.

Correctts
const activity = useRpcQuery("users.activity", { id: "1", tab: "a" });

Incomplete input#

Incorrectts
const activity = useRpcQuery("users.activity", { id: "1" });

meeko check reports TS2345: Argument of type '{ id: string; }' is not assignable to parameter of type 'RpcQueryInput<{ id: string; tab: string; }>', with Property 'tab' is missing.

Correctts
const activity = useRpcQuery("users.activity", { id: "1", tab: "all" });

Call it inside an effect#

Incorrectts
effect(() => {
  useRpcQuery("users.activity", { id: "1", tab: tab.value });
});

meeko check reports MEEK054: useRpcQuery() debe llamarse en el nivel superior de <script>, no dentro de una función, un handler, un effect ni onMounted. Reloading is already reactive: pass a function reading the signal.

Correctts
const activity = useRpcQuery("users.activity", () => ({ id: "1", tab: tab.value }));

Input the browser cannot reproduce#

Incorrectts
const feed = await useRpcQuery("feed.latest", { since: Date.now() });

The server requests data with its own Date.now() and the browser uses another during hydration: the key does not match and hydration fails with MEEKO_RESOURCE_MISSING: useRpcQuery("feed.latest") (…) is not in the page payload; the server and the browser requested different data…. Make input depend only on props, route data or state:

Correctts
const feed = await useRpcQuery("feed.latest", { since: props.data.since });

Notes#

  • SSR and hydration. Like useApi: with await, the server waits for data (at most 10 passes) and sends it in resources; without await, the request is never made on the server.
  • Request key. The procedure and input in stable JSON form the application cache key: a component mounting later with the same input reuses the data.
  • Session and security. The request carries the user's session; the procedure applies its guards (auth, authorize). Validate everything arriving in input on the server.
  • Writes. useRpcQuery is for reading: it may repeat during navigation, hydration or input changes. To change data, use useRpcMutation.

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-query.md