Skip to main content

Queries, Mutations & Actions

For AI agents: see llms.txt for the complete documentation index. Markdown versions are available by adding .md to a page URL or requesting Accept: text/markdown.

This page covers the core reactive API of convex-svelte. Everything here works in any Svelte app — SvelteKit, Vite + Svelte, or any other setup.

Make sure you've called setupConvex() in a root layout first — see Overview.

Two clients​

Convex Svelte talks to your backend through two clients, and the one you use decides what's available:

ConvexClient (WebSocket)ConvexHttpClient (fetch)
QueriesuseQuery().query()
MutationsuseMutation() — optimistic updates.mutation()
ActionsuseAction().action()
Live subscriptions✓-

ConvexClient is the live WebSocket client that setupConvex() opens — the useQuery / useMutation / useAction helpers are thin Svelte wrappers around it, and you can retrieve it directly with getConvexClient() / useConvexClient(). ConvexHttpClient is a stateless fetch-based client for single calls from server code or scripts — see One-time calls. Live subscriptions and optimistic updates are WebSocket-only, and optimistic updates apply to mutations only.

Queries​

Use useQuery() to subscribe to a Convex query with automatic real-time updates. When the data changes on the server, your component re-renders automatically.

<script lang="ts">
import { useQuery } from "convex-svelte";
import { api } from "../convex/_generated/api.js";

const messages = useQuery(api.messages.list, () => ({ searchWords }), {
keepPreviousData: true,
});
</script>

{#if messages.isLoading}
Loading...
{:else if messages.error != null}
failed to load: {messages.error.toString()}
{:else}
<ul>
{#each messages.data as message}
<li>
<span>{message.author}</span>
<span>{message.body}</span>
</li>
{/each}
</ul>
{/if}

The returned object is reactive and has the following shape:

PropertyTypeDescription
dataT | undefinedThe query result, or undefined while loading
errorError | undefinedThe error, if the query failed
isLoadingbooleantrue until the first result or error is received
isStalebooleantrue when displaying cached data from previous arguments

Options​

  • initialData — pre-loaded data for SSR/hydration, avoids the loading state (see SSR with initialData)
  • keepPreviousData — when true, keeps displaying the previous result while new data loads after args change

Skipping queries​

You can conditionally skip a query by returning 'skip' from the arguments function. This is useful when a query depends on some condition, like authentication state or user input.

<script lang="ts">
import { useQuery } from "convex-svelte";
import { api } from "../convex/_generated/api.js";

let auth = $state({ isAuthenticated: true });

const user = useQuery(api.users.queries.getActiveUser, () =>
auth.isAuthenticated ? {} : "skip",
);
</script>

{#if user.isLoading}
Loading user...
{:else if user.error}
Error: {user.error}
{:else if user.data}
Welcome, {user.data.name}!
{/if}

When a query is skipped, isLoading will be false, error will be null, and data will be undefined.

Mutations & Actions​

Use useMutation() and useAction() to get callable functions for your Convex mutations and actions. Both use the module-level singleton (getConvexClient()) internally, so they work in .svelte components and plain .ts / .js files — anywhere after setupConvex() has been called.

<script lang="ts">
import { useMutation } from "convex-svelte";
import { api } from "../convex/_generated/api.js";

const sendMessage = useMutation(api.messages.send);

let toSend = $state("");
let author = $state("me");

function handleSubmit(event: SubmitEvent) {
event.preventDefault();

const data = Object.fromEntries(
new FormData(event.target as HTMLFormElement).entries(),
);
sendMessage({
author: data.author as string,
body: data.body as string,
});
}
</script>

<form onsubmit={handleSubmit}>
<input type="text" name="author" bind:value={author} />
<input type="text" name="body" bind:value={toSend} />
<button type="submit" disabled={!toSend}>Send</button>
</form>

Actions are similar to mutations but can have side effects like calling third-party APIs:

import { useAction } from "convex-svelte";
import { api } from "../convex/_generated/api.js";

const generateUploadUrl = useAction(api.files.generateUploadUrl);
const uploadUrl = await generateUploadUrl({});

Optimistic updates​

Optimistic updates let you update the UI immediately when a mutation is called, without waiting for the server to respond. Pass an optimisticUpdate callback in the mutation options at the call site to update the local query cache.

<script lang="ts">
import { useMutation } from "convex-svelte";
import { api } from "../convex/_generated/api.js";

const updateUser = useMutation(api.user.update);

async function handleUpdate() {
await updateUser(
{ name: "John Doe" },
{
optimisticUpdate: (store) => {
store.setQuery(api.user.get, {}, { name: "John Doe" });
},
},
);
}
</script>

Inside the optimisticUpdate callback, use store.setQuery() to update the local cache for a specific query. The arguments are:

  1. Query reference — the query to update (e.g. api.user.get)
  2. Query arguments — must match the arguments used by the active useQuery() subscription
  3. New value — the optimistic data to display immediately

If the mutation fails, the optimistic update is automatically rolled back and the UI reverts to the server state.

Client access​

getConvexClient() — universal client access​

getConvexClient() retrieves the client from a module-level singleton. It works anywhere — .svelte components, plain .ts utility files, service layers, async callbacks — as long as setupConvex() has been called first.

This is the recommended way to access the client outside of the layout where setupConvex() returns it directly.

useConvexClient() — Svelte context alternative​

useConvexClient() retrieves the same client from Svelte context via getContext(). It only works during component initialization — inside .svelte files or code called synchronously from a component's <script> block. Both functions return the same ConvexClient instance.

getConvexClient()useConvexClient()
Works inAnywhere (.ts, .svelte, hooks)Svelte components only
MechanismModule singletonSvelte getContext()

Using mutations in utility files​

useMutation() and useAction() work in plain .ts files too, since they use the module-level singleton:

src/lib/services/tasks.ts
import { useMutation } from "convex-svelte";
import { api } from "../convex/_generated/api.js";

const createTaskMutation = useMutation(api.tasks.create);
const completeTaskMutation = useMutation(api.tasks.complete);

export async function createTask(text: string) {
await createTaskMutation({ text });
}

export async function completeTask(id: string) {
await completeTaskMutation({ id });
}

Then call these functions from any component without plumbing the client through:

<script lang="ts">
import { createTask } from "$lib/services/tasks.js";

let text = $state("");
</script>

<form
onsubmit={(e) => {
e.preventDefault();
createTask(text);
text = "";
}}
>
<input bind:value={text} />
<button type="submit">Add</button>
</form>
note

The .svelte.ts file extension enables Svelte 5 runes ($state, $derived, $effect) but does not make getContext() work outside components. If you need the client in a plain .ts file, use getConvexClient(), not useConvexClient().

One-time calls​

useQuery keeps a live subscription open. When you instead need a single result with no ongoing binding — a SvelteKit load function, a form action, an endpoint, or a one-off script — use the stateless ConvexHttpClient, which runs query / mutation / action over fetch with no WebSocket.

import { ConvexHttpClient } from "convex/browser";
import { api } from "../convex/_generated/api.js";

const client = new ConvexHttpClient(process.env.CONVEX_URL!);
const count = await client.query(api.tasks.count, {});

In SvelteKit, the createConvexHttpClient() helper builds one with per-request auth wired in. The HTTP client supports one-shot calls only — no subscriptions and no optimistic updates.

Mutations and actions have no "live" form to opt out of: useMutation() and useAction() are already one-shot calls (thin wrappers over the WebSocket client), so they cover writes in both reactive and non-reactive code.

Paginated queries​

For queries that return large datasets, use usePaginatedQuery() to load results incrementally. This hook manages cursor-based pagination automatically and provides a loadMore function to fetch additional pages.

<script lang="ts">
import { usePaginatedQuery } from "convex-svelte";
import { api } from "../convex/_generated/api.js";

const paginatedMessages = usePaginatedQuery(
api.messages.listPaginated,
() => ({}),
{
initialNumItems: 10,
},
);
</script>

{#if paginatedMessages.isLoading}
Loading...
{:else if paginatedMessages.error}
Error: {paginatedMessages.error.toString()}
{:else}
<ul>
{#each paginatedMessages.results as message}
<li>
<span>{message.author}</span>
<span>{message.body}</span>
</li>
{/each}
</ul>
{#if paginatedMessages.status === "CanLoadMore"}
<button onclick={() => paginatedMessages.loadMore(10)}>Load more</button>
{/if}
{/if}

Options​

  • initialNumItems (required) — number of items to load on the first page
  • initialData — optional initial data for SSR/hydration
  • keepPreviousData — when true, keeps previous results visible while loading new data after args change

You can also skip a paginated query by returning 'skip' from the arguments function, just like with useQuery().

<script lang="ts">
import { usePaginatedQuery } from "convex-svelte";
import { api } from "../convex/_generated/api.js";

let searchTerm = $state("");

const searchResults = usePaginatedQuery(
api.messages.search,
() => (searchTerm.length > 0 ? { query: searchTerm } : "skip"),
{ initialNumItems: 20, keepPreviousData: true },
);
</script>

API reference​

Functions and types exported from convex-svelte:

ExportKindDescription
setupConvex(url, options?)FunctionInitialize the Convex client and store it in Svelte context. Call once in a root layout. Returns ConvexClient.
useConvexClient()FunctionRetrieve the ConvexClient from Svelte context. Must be called during component initialization.
getConvexClient()FunctionRetrieve the ConvexClient module singleton. Works anywhere — no Svelte context needed.
closeConvex()FunctionClose the app-scoped client and clear the singleton. For explicit teardown, e.g. in tests. Returns Promise.
useQuery(query, args, options?)FunctionSubscribe to a Convex query with reactive updates. Returns UseQueryReturn.
UseQueryOptions<Query>TypeOptions for useQuery: initialData, keepPreviousData.
UseQueryReturn<Query>TypeReturn type of useQuery: data, error, isLoading, isStale.
usePaginatedQuery(query, args, options)FunctionSubscribe to a paginated Convex query with cursor management. Returns UsePaginatedQueryReturn.
UsePaginatedQueryOptions<Query>TypeOptions for usePaginatedQuery: initialNumItems, initialData, keepPreviousData.
UsePaginatedQueryReturn<Query>TypeReturn type of usePaginatedQuery: results, status, isLoading, loadMore, error.

Authentication exports (setupAuth, useAuth, and related types) are documented on the Authentication page.