[!NOTE] The README on npmjs might not be fully up to date. Please refer to the README on the Github Repo for the latest setup instructions.
An adapter to call tRPC
procedures wrapped with @tanstack/svelte-query
, similar to @trpc/react-query
. This is made possible using proxy-deep
.
# npm
npm install trpc-svelte-query-adapter @trpc/client @trpc/server @tanstack/svelte-query
# yarn
yarn add trpc-svelte-query-adapter @trpc/client @trpc/server @tanstack/svelte-query
# pnpm
pnpm add trpc-svelte-query-adapter @trpc/client @trpc/server @tanstack/svelte-query
If you are using client-side Svelte, you would need to install @trpc/server
as a devDependency
using --save-dev
.
The following functions from @trpc/react-query
are ported over:
-
useQuery
->createQuery
-
useInfiniteQuery
->createInfiniteQuery
-
useMutation
->createMutation
-
useSubscription
->createSubscription
-
useQueries
->createQueries
-
useUtils
->createUtils
getQueryKey
You can refer to tanstack-query docs
and @trpc/react-query docs
for documentation on how to use them.
There are also some new procedures that are only relevant for SvelteKit:
createServerQuery
createServerInfiniteQuery
createServerQueries
As for these procedures, you can refer to the Server-Side Query Pre-Fetching section.
The following instructions assume the tRPC
router to have the following procedures:
export const router = t.router({
greeting: t.procedure
.input((name: unknown) => {
if (typeof name === 'string') return name;
throw new Error(`Invalid input: ${typeof name}`);
})
.query(async ({ input }) => {
return `Hello, ${input} from tRPC v10 @ ${new Date().toLocaleTimeString()}`;
}),
});
export type Router = typeof router;
- Setup
@tanstack/svelte-query
as per svelte-query docs. - Setup
@trpc/client
and export thetRPC
client. - Wrap the exported
tRPC
client withsvelteQueryWrapper
fromtrpc-svelte-query-adapter
, as demonstrated in the example below:
// src/lib/trpc.ts
import type { Router } from '/path/to/trpc/router';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { svelteQueryWrapper } from 'trpc-svelte-query-adapter';
const client = createTRPCProxyClient<Router>({
links: [
httpBatchLink({
// Replace this URL with that of your tRPC server
url: 'http://localhost:5000/api/v1/trpc/',
}),
],
});
export const trpc = svelteQueryWrapper<Router>({ client });
- The exported
tRPC
client can then be used insvelte
components as follows:
<script lang="ts">
import { trpc } from "/path/to/lib/trpc";
const foo = trpc.greeting.createQuery('foo', { retry: false });
</script>
{#if $foo.isPending}
Loading...
{:else if $foo.isError}
Error: {$foo.error.message}
{:else if $foo.data}
{$foo.data.message}
{/if}
For SvelteKit, the process is pretty much the same as for client-only svelte. However, if you intend to call queries from the server in a load
function, you would need to setup @tanstack/svelte-query
according to the the ssr example in the svelte-query docs.
Upon doing that, you would also need to pass in the queryClient
to svelteQueryWrapper
when initializing on the server, which you can get by calling the event.parent
method in the load
function. You can see an example of this in the Server-Side Query Pre-Fetching section. For this purpose, you might also want to export your client wrapped in a function that optionally takes in queryClient
and passes it onto svelteQueryWrapper
.
Here is an example of what that might look like:
import type { QueryClient } from '@tanstack/svelte-query';
const client = createTRPCProxyClient<Router>({
links: [
httpBatchLink({
// Replace this URL with that of your tRPC server
url: 'http://localhost:5000/api/v1/trpc/',
}),
],
});
export function trpc(queryClient?: QueryClient) {
return svelteQueryWrapper<Router>({
client,
queryClient,
});
}
Which can then be used in a component as such:
<!-- routes/+page.svelte -->
<script lang="ts">
import { trpc } from "$lib/trpc/client";
const client = trpc();
const foo = client.greeting.createQuery("foo", { retry: false });
</script>
<p>
{#if $foo.isPending}
Loading...
{:else if $foo.isError}
Error: {$foo.error.message}
{:else}
{$foo.data}
{/if}
</p>
The main thing that needs to passed in to svelteQueryWrapper
is the tRPC
client itself. So, this adapter should support different implementations of tRPC
for Svelte and SvelteKit. For example, if you are using trpc-sveltekit by icflorescu
, all you would need to do after setting it up would be to change the client initialization function from something like this:
let browserClient: ReturnType<typeof createTRPCClient<Router>>;
export function trpc(init?: TRPCClientInit) {
const isBrowser = typeof window !== 'undefined';
if (isBrowser && browserClient) return browserClient;
const client = createTRPCClient<Router>({ init });
if (isBrowser) browserClient = client;
return client;
}
to this:
import { svelteQueryWrapper } from 'trpc-svelte-query-adapter';
import type { QueryClient } from '@tanstack/svelte-query';
let browserClient: ReturnType<typeof svelteQueryWrapper<Router>>;
export function trpc(init?: TRPCClientInit, queryClient?: QueryClient) {
const isBrowser = typeof window !== 'undefined';
if (isBrowser && browserClient) return browserClient;
const client = svelteQueryWrapper<Router>({
client: createTRPCClient<Router>({ init }),
queryClient,
});
if (isBrowser) browserClient = client;
return client;
}
Which can then be initialized and used in the way that it is described in its docs.
This adapter provides 3 additional procedures: createServerQuery
, createServerInfiniteQuery
and createServerQueries
, which can be used to call their counterpart procedures in the load
function in either a +page.ts
or +layout.ts
. These procedures return a promise
and therefore can only really be called on the server.
By default, these 3 procedures will pre-fetch the data required to pre-render the page on the server. However, if you wish to disable this behaviour on certain queries, you can do so by setting the ssr
option to false
.
These procedures can be used as such:
[!NOTE] Gotta await top-level promises to pre-fetch data from SvelteKit v2.
// +page.ts
// tRPC is setup using `trpc-sveltekit` for this example.
import { trpc } from '$lib/trpc/client';
import type { PageLoad } from './$types';
export const load = (async (event) => {
const { queryClient } = await event.parent();
const client = trpc(event, queryClient);
return {
foo: await client.greeting.createServerQuery('foo'),
queries: await client.createServerQueries(
(t) =>
['bar', 'baz'].map((name) => t.greeting(name, { ssr: name !== 'baz' })) // pre-fetching disabled for the `baz` query.
),
};
}) satisfies PageLoad;
Then, in the component:
<!-- +page.svelte -->
<script lang="ts">
import { page } from "$app/stores";
import type { PageData } from "./$types";
export let data: PageData;
const foo = data.foo();
const queries = data.queries();
</script>
{#if $foo.isPending}
Loading...
{:else if $foo.isError}
{$foo.error}
{:else if $foo.data}
{$foo.data}
{/if}
<br /><br />
{#each $queries as query}
{#if query.isPending}
Loading...
{:else if query.isError}
{query.error.message}
{:else if query.data}
{query.data}
{/if}
<br />
{/each}
You can also optionally pass new inputs to the queries and infinite queries from the client side(see #34, #47) like so:
<script lang="ts">
import { page } from "$app/stores";
import type { PageData } from "./$types";
import { derived, writable } from '@svelte/store';
export let data: PageData;
const name = writable('foo');
const newNames = writable<string[]>([]);
const foo = data.foo($name);
// You can also access the default input if you pass in a callback as the new input:
// const foo = data.foo((old) => derived(name, ($name) => old + name));
const queries = data.queries((t, old) => derived(newNames, ($newNames) => [...old, ...$newNames.map((name) => t.greeting(name))]));
</script>
<div>
{#if $foo.isPending}
Loading...
{:else if $foo.isError}
{$foo.error}
{:else if $foo.data}
{$foo.data}
{/if}
<input bind:value={$name} />
</div>
<br />
<div>
{#each $queries as query}
{#if query.isPending}
Loading...
{:else if query.isError}
{query.error.message}
{:else if query.data}
{query.data}
{/if}
<br />
{/each}
<form on:submit|preventDefault={(e) => {
const data = new FormData(e.currentTarget).get('name');
if (typeof data === 'string') $newNames.push(data);
$newNames = $newNames;
}}>
<input name="name" />
<button type="submit">Submit</button>
</form>
</div>
For more usage examples, you can refer to the example app provided in the repo.