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 reactive API of convex-svelte. It works in any Svelte app, with or without SvelteKit, and assumes setupConvex() is in a root layout as shown in Overview.

Two clients​

Convex Svelte talks to your backend through two clients, and the one you use decides what you can do:

ConvexClient (WebSocket)ConvexHttpClient (fetch)
QueriesuseQuery().query()
MutationsuseMutation(), optimistic updates.mutation()
ActionsuseAction().action()
Live subscriptionsYesNo

ConvexClient is the WebSocket client that setupConvex() opens in the browser. useQuery(), useMutation() and useAction() wrap it, and getConvexClient() and useConvexClient() return it. On the server it never connects, so server code and scripts use ConvexHttpClient, a stateless client that sends each call with fetch (see One-time calls). Live subscriptions and optimistic updates need the WebSocket client.

Queries​

Use useQuery() to subscribe to a Convex query. When the data changes on the server, the component updates.

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

let searchWords = $state<string[]>([]);

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: data to show before the first result, for example from a server load, so there's no loading state (see SSR with initialData).
  • keepPreviousData: when true, keeps showing the previous result while the query loads for new arguments.

Skipping queries​

To skip a query, return 'skip' from the arguments function, for example while the user isn't signed in or hasn't typed anything yet.

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

const auth = useAuth();

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}

While a query is skipped, isLoading is false, error is undefined, and data is undefined. With keepPreviousData, data keeps the last result instead.

Mutations & Actions​

useMutation() and useAction() return functions that call your Convex mutations and actions. Both use the client from getConvexClient(), so they work in .svelte components and in plain .ts / .js files in the browser, once setupConvex() has run. On the server, use createConvexHttpClient() instead.

<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​

An optimistic update changes the UI as soon as you call a mutation, before the server responds. Pass an optimisticUpdate callback in the mutation options at the call site to change the local query results.

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

In the callback, store.setQuery(query, args, value) sets the local result of a query. args must match the arguments of the active useQuery() subscription. If the mutation fails, Convex rolls the optimistic update back and the UI shows the server state again.

Client access​

setupConvex() returns the client in the root layout. Everywhere else, two functions return the same ConvexClient:

  • getConvexClient() reads it from a module-level singleton. It works in components, plain .ts files and async callbacks in the browser, once setupConvex() has run. Use it by default.
  • useConvexClient() reads it from Svelte context with getContext(), so it only works while a component initializes: in a component's <script> block, or in code called synchronously from there.

On the server the client never connects, so server code uses createConvexHttpClient().

Using mutations in utility files​

useMutation() and useAction() work in plain .ts files too:

src/lib/services/tasks.ts
import { useMutation } from "convex-svelte";
import { api } from "../../convex/_generated/api.js";
import type { Id } from "../../convex/_generated/dataModel.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: Id<"tasks">) {
await completeTaskMutation({ id });
}

Then call these functions from any component, without passing the client around:

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

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 getContext() still only works in components. In a .ts or .svelte.ts file, use getConvexClient(), not useConvexClient().

One-time calls​

useQuery() keeps a live subscription open. For a single result without a subscription:

  • In SvelteKit load functions, use convexLoad(). It renders on the server and then keeps the result live in the browser.
  • In other SvelteKit server code, such as form actions and endpoints, use createConvexHttpClient(). It passes the request's auth token for you.
  • In scripts and outside SvelteKit, use ConvexHttpClient, which runs query, mutation and action over fetch, without a 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, {});

The HTTP client has no subscriptions and no optimistic updates. In the browser, useMutation() and useAction() already make single calls, so writes there don't need it.

Paginated queries​

For queries that return many results, usePaginatedQuery() loads them page by page. It keeps track of the cursors, and loadMore() loads the next page.

<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): the number of items on the first page.
  • initialData: the first page, for example from a server load.
  • keepPreviousData: when true, keeps showing the previous results while the query loads for new arguments.

To skip a paginated query, return 'skip' from the arguments function, as 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​

The main 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 in browser code without Svelte context.
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.