Skip to main content

Queries, Mutations & Actions

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.