vishalbalaji / trpc-svelte-query-adapter Goto Github PK
View Code? Open in Web Editor NEWA simple adapter to use `@tanstack/svelte-query` with trpc, similar to `@trpc/react-query`.
A simple adapter to use `@tanstack/svelte-query` with trpc, similar to `@trpc/react-query`.
Thanks for this library first of all, it's great! ๐
Instead of doing this:
export const load: PageLoad = async (event) => {
const { queryClient } = await event.parent();
const api = trpc(event, queryClient);
return {
queries: await api.createServerQueries((t) => [
t.public.todos.list(),
t.public.hello.get()
])
};
};
I'd like to be able to name the queries like so:
export const load: PageLoad = async (event) => {
const { queryClient } = await event.parent();
const api = trpc(event, queryClient);
return {
queries: await api.createServerQueries((t) => [
{
listTodos: t.public.todos.list(),
getHello: t.public.hello.get()
}
])
};
};
Right now i'm doing something like this on the frontend
<script lang="ts">
import { page } from '$app/stores';
import { trpc } from '$lib/trpc/client';
import type { PageData } from './$types';
export let data: PageData;
const queries = data.queries();
$: listTodos = $queries[0];
$: getHello = $queries[1];
const api = trpc($page);
const addTodo = api.public.todos.add.createMutation();
const deleteTodo = api.public.todos.delete.createMutation();
</script>
I'd love to be able to access the queries directly like so:
$queries.listTodos
and
$queries.getHello
Let's say I'm using createServerQuery
to prefetch a list of users on the server and pass the query to the client:
// +page.ts
import type { PageLoad } from './$types';
import { trpc } from '$lib/trpc/client';
export const load: PageLoad = async (event) => {
const { queryClient } = await event.parent();
const client = trpc(event, queryClient);
return {
users: await client.user.all.createServerQuery(),
};
};
I can use the query result to display content accordingly:
<!-- +page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
const users = data.users();
</script>
<h1>List of users</h1>
{#if $users.isFetching}
<p>Loading...</p>
{:else if $users.error}
<p>Error: {$users.error.message}</p>
{:else if $users.isSuccess}
<ul>
{#each $users.data as user}
<li>{user.name}</li>
{/each}
</ul>
{:else}
<p>No users found</p>
{/if}
Now I want to filter the list of users as someone types in an input field. This requires me to send an input to the query. My guess was I would be able to pass the input to data.users()
just like I can pass the input to trpc($page).user.all.createQuery(input)
but this does not work and I could not find an example to achieve this.
<!-- +page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let filter = '';
$: users = data.users({ name: filter });
</script>
<input type="text" bind:value={filter} placeholder="Filter users" />
...
I might be missing something really obvious here as this seems to me like a very common use case: fetching data on the server, passing it to the client so that the client doesn't have to load it again when it hydrates and then the subsequent loading of data happens on the client when inputs vary depending on user actions.
Apologies if this has been already answered somewhere, I dug into the README and issues but couldn't find anything.
Outstanding work, @vishalbalaji! Looks great!
Would you be interested in publishing your package to npm
or contributing this source code to trpc-sveltekit
?
Firstly, thanks so much for making this adapter.
I'm trying to cancel a query but I'm not observing any effect from my attempts. Reading the trpc docs for the 'useQuery' function, they mention needing to set 'abortOnUnmount' to true, but I can't find a way to set this using this adapter. Am I missing something?
Anyone have a working example they can share? ๐
This deserves way more love.
I removed os much boilerplate and repetitiveness using this ๐
In trpc-sveltekit
, the following is possible (from https://icflorescu.github.io/trpc-sveltekit/getting-started):
<script lang="ts">
import { page } from '$app/stores';
import { trpc } from '$lib/trpc/client';
let greeting = 'press the button to load data';
let loading = false;
const loadData = async () => {
loading = true;
greeting = await trpc($page).greeting.query(); // (substituted with `createQuery()`)
loading = false;
};
</script>
<h6>Loading data in<br /><code>+page.svelte</code></h6>
<a
href="#load"
role="button"
class="secondary"
aria-busy={loading}
on:click|preventDefault={loadData}>Load</a
>
<p>{greeting}</p>
Attempting to implement this pattern with trpc-svelte-query-adapter
results in an error of Function called outside component initialization
. It seems due to the fact that svelte-query
's createQuery
method is called (by trpc-svelte-query-adapter
) without it being passed a queryClient
, so it tries to get it from the current component context, which of course doesn't exist outside of component initialization.
Is there a different way to implement this pattern, or would modifying these lines to pass in a queryClient
be better?
Thank you for the awesome lib :)
So in your ssr example, you returned the query function from page.ts
// page.ts->load()
return {
foo: await client.greeting.createServerQuery('foo')
}
And then used used the returned function instead of greeting.createQuery
export let data: PageData;
const foo = data.foo();
I realized once i called createServerQuery
inside layout load, my createQuery was doing ssr fine. So is there a reason you did this the way you did it
client.greeting.createQuery('foo')
One more question, my queries seem to refetch on mount regardless of what i set to createServerQuery
or createQuery
, is this related to this adapter? Can you point me to where should i look to resolve this?
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
enabled: false,
On updating to [email protected]
, trpc
is not using the proper fetch
causing trpc
to throw errors and the queries happening twice, on both server and client. Needs to be looked into.
how to get the original trpc client to get data on the server load/actions or inside +server.ts files?
when using trpc.procedure.useMutation({
onMutate() {},
onError() {},
...rest
})
trpc.procedure.useMutation(undefined, {
onMutate() {},
onError() {},
...rest
}) seems to be working but then typescript shows error
I get Unexpected token '<', "<!DOCTYPE "... is not valid JSON
when running the example.
i figure its the httpBatchLink
that is the culprit here, but i can't figure out what this url is supposed to point to.
I read the tRPC docs and cannot seem to wrap my mind around this.
I thought maybe you could help
I think getQueryKey is returning the wrong key?
setup
<script lang="ts">
import { page } from '$app/stores';
import { useQueryClient } from '@tanstack/svelte-query'
import { trpc } from '$lib/trpc/client';
import { writable } from "svelte/store";
import clonedeep from "lodash.clonedeep";
const queryClient = useQueryClient();
const utils = trpc($page, queryClient).createUtils();
let id = 5;
const greeting = trpc($page, queryClient).greeting.createQuery({
where: {
id: id
}
});
$: {
if($greeting?.status === 'success'){
const queriesData = queryClient.getQueriesData();
console.log("queriesData",queriesData);
const queryKey = trpc($page, queryClient).greeting.getQueryKey({
where: {
id: id
}
});
console.log("queryKey", queryKey);
const data = utils.greeting.getData();
console.log("data",data);
console.log("queryKey from utils", queryKey)
//no data
console.log('no data :(', queryClient.getQueryData(queryKey));
const newQueryKey = clonedeep(queryKey);
console.log("newQueryKey", newQueryKey)
//data
newQueryKey[1]['type'] = 'query';
console.log('data found :D', queryClient.getQueryData(newQueryKey))
}
}
</script>
{#if $greeting.isLoading}
loading...
{:else}
{$greeting.data}
{/if}
<h6>Loading data in<br /><code>+page.svelte</code></h6>
Am I doing something wrong? I have made stackblitz
https://stackblitz.com/edit/sveltejs-kit-template-default-thhat3?description=The%20default%20SvelteKit%20template,%20generated%20with%20create-svelte&file=src%2Froutes%2F%2Blayout.svelte,src%2Froutes%2F%2Bpage.svelte,src%2Flib%2Ftrpc%2Frouter.ts,src%2Flib%2Ftrpc%2Fcontext.ts,src%2Fhooks.server.ts,src%2Flib%2Ftrpc%2Fclient.ts,src%2Fapp.html&title=SvelteKit%20Default%20Template
When I am accessing CreateQuery in +layout.svelte
it is showing an error of No QueryClient in svelte context. In addition I am not able to access the CreateServerQuery result in +layout.svelte
. I am using trpc-sveltekit by @icflorescu. Here is the code
// lib/trpc/trpc.ts
import { svelteQueryWrapper } from '$lib/trpc-svelte-query';
import type { QueryClient } from '@tanstack/svelte-query';
import type { Router } from '$lib/trpc/router';
import {createTRPCClient, type TRPCClientInit } from 'trpc-sveltekit';
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;
}
// +layout.ts
import { QueryClient } from "@tanstack/svelte-query";
import type { LayoutLoad } from "./$types";
import type { LoadEvent } from "@sveltejs/kit";
import { trpc } from "$lib/trpc/client";
export const load: LayoutLoad = async (event: LoadEvent) => {
const queryClient = new QueryClient()
const client = trpc(event, queryClient)
return {queryClient, grettingData: client.greeting.createServerQuery()}
}
// +layout.svelte
<script lang="ts">
import type { LayoutData } from "./$types";
import {QueryClientProvider, useQueryClient} from "@tanstack/svelte-query"
export let data: LayoutData;
const greetingData = data.grettingData()
console.log($greetingData.data)
</script>
<QueryClientProvider client={data.queryClient}>
<slot />
</QueryClientProvider>
When using .createQuery
, .createInfiniteQuery
, and every other function that takes opts?:
, the queryKey is always required
For example this has no type errors:
const search = trpc($page).search.text.createQuery({ text: input.trim() })
But this has an error:
// Property 'queryKey' is missing in type '{ enabled: boolean; }' but required in type ...
const search = trpc($page).search.text.createQuery({ text: input.trim() }, { enabled: input.trim().length > 0 })
In the dist, queryKey
is always overridden so there is no reason why queryKey
should even be available
My suggestion is adding Partial<> to each of the opts?:
however you know more than me if this is a good idea
Instead of doing the following (to prerender queries on the server):
// +page.ts
import { trpc } from '$lib/trpc/client';
import type { PageLoad } from './$types';
export const load: PageLoad = async (event) => {
const { queryClient } = await event.parent();
const api = trpc(event, queryClient);
return {
queries: await api.createServerQueries((t) => [
t.authed.todos.all(),
t.public.hello.get()
])
};
};
I'd like to be able to stream the result of queries
for example on my page. I'm new to Svelte, but I know that Svelte does support streaming data via promises. Apparently if you don't await the data in the server load function, you can use the await
block on the client to show a loading fallback while the promise resolves.
For example (From: https://kit.svelte.dev/docs/load#streaming-with-promises):
// +page.svelte.ts
{#await data.streamed.comments}
Loading...
{:then value}
{value}
{/await}
In relation to createServerQueries
, is there a way I can do streaming with it, so I can have faster page loads? Any advice/guidance would be super appreciated!
createInfiniteQuery
has the incorrect type for data
For example
// route.ts
router({
infinite: protectedProcedure
.use(logger)
.input(
z.object({
cursor: z.number(),
})
)
.query(async ({ input, ctx }) => {
console.log(input)
const { cursor } = input
const posts = await db.query.clubPosts.findMany({
limit: limit + 1,
offset: cursor,
...
orderBy: (post, { desc }) => [desc(post.createdAt)],
})
let nextCursor: number | null = null
if (posts.length > limit) {
posts.pop()
nextCursor = cursor + limit
}
return { posts, nextCursor }
}),
})
// page.svelte
const feed = trpc($page).explore.infinite.createInfiniteQuery(
{},
{
initialPageParam: 0,
getNextPageParam: ({ nextCursor }) => nextCursor,
}
)
$feed.data
incorrectly shows the raw query return type, rather than pageParams
and pages
This can be fixed by wrapping InfiniteData
on TData
in the d.ts
heya thanks for the lib.
Am I right thinking that if I want to use trpc on a server code, I need an entirely different isntance?
eg.
// $lib/trpc.ts
import type { AppRouter } from '$lib/server/trpc/routes';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import SuperJSON from 'superjson';
import { svelteQueryWrapper } from 'trpc-svelte-query-adapter';
import type { QueryClient } from '@tanstack/svelte-query';
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:5173/api/trpc'
})
],
transformer: SuperJSON
});
export const trpc = (queryClient?: QueryClient) =>
svelteQueryWrapper<AppRouter>({
client,
queryClient
});
export const trpcOnServer = (fetch) =>
createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: '/api/trpc',
fetch
})
],
transformer: SuperJSON
});
// +page.server.ts
import type { Actions } from './$types';
import { trpcOnServer } from '$lib/trpc';
export const actions: Actions = {
default: async ({ request, fetch }) => {
const data = await request.formData();
const email = data.get('email');
const api = trpcOnServer(fetch);
const [newProject] = await api.projects.save.mutate({
email
});
}
};
I have the following procedure:
export const userRouter = {
all: publicProcedure
.input(
z
.object({
name: z.string(),
})
.optional(),
)
.query(async ({ input }) => {
return 'something'
}),
} satisfies TRPCRouterRecord;
Trying to get the query key gives me the following type error:
Seems like the type GetQueryKey
is not accounting for the void
type
type GetQueryKey<TInput = undefined> = TInput extends undefined
? {
[ProcedureNames.queryKey]: () => QueryKey
}
: {
/**
* Method to extract the query key for a procedure
* @param type - defaults to `any`
*/
[ProcedureNames.queryKey]: (input: TInput, type?: QueryType) => QueryKey
} & {}
This can be fixed by modifying the type as:
type GetQueryKey<TInput = undefined> = [TInput] extends [undefined] | [void]
? {
[ProcedureNames.queryKey]: () => QueryKey
}
: {
/**
* Method to extract the query key for a procedure
* @param type - defaults to `any`
*/
[ProcedureNames.queryKey]: (input: TInput, type?: QueryType) => QueryKey
} & {}
If it looks good, I'll be happy to create a PR for this fix.
P.S. Great work on this package! ๐
The list of features that need to be implemented from @trpc/react-query
according to trpc docs and personal testing are as follows:
useQuery
useMutation
useInfiniteQuery
useSubscription
getQueryKey
useContext
fetch
prefetch
fetchInfinite
prefetchInfinite
invalidate
refetch
reset
cancel
setData
getData
setInfiniteData
getInfiniteData
useQueries
Is this peer dependency necessary? (I'm genuinely not sure).
It's blocking linting upgrades in my project without applying --force
to the install.
Example:
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR!
npm ERR! While resolving: [email protected]
npm ERR! Found: @typescript-eslint/[email protected]
npm ERR! node_modules/@typescript-eslint/eslint-plugin
npm ERR! dev @typescript-eslint/eslint-plugin@"^7.1.1" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer @typescript-eslint/eslint-plugin@"^6.10.0" from [email protected]
npm ERR! node_modules/trpc-svelte-query-adapter
npm ERR! trpc-svelte-query-adapter@"^2.2.5" from the root project
npm ERR!
npm ERR! Conflicting peer dependency: @typescript-eslint/[email protected]
npm ERR! node_modules/@typescript-eslint/eslint-plugin
npm ERR! peer @typescript-eslint/eslint-plugin@"^6.10.0" from [email protected]
npm ERR! node_modules/trpc-svelte-query-adapter
npm ERR! trpc-svelte-query-adapter@"^2.2.5" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
I was having error while trying to use this library with @tanstack/svelte-query v5.
This library imports some stuff that aren't available in the @tanstack/svelte-query v5. Specifically:ResetQueryFilters
and CreateQueriesResult
I have the following code in a /+page.ts
export const load = (async (evt) => {
const { queryClient } = await evt.parent();
const client = trpcQuery(evt, queryClient);
return {
data: client.test.list.createServerQuery({}, { staleTime: 10 * 60 }),
};
}) satisfies PageLoad;
Every time I navigate to the page the query is refetched and the staleTime is not used.
I understand that If I refresh the browser the queryClient is cleared out, but I was expecting the stale time to respected for client-side navigation.
Is there a setting where I can enable respecting staleTime for client-side navigation?
Seems like the variables are not sent while using useMutation(), I am not sure if those are send on useQuery() or not but for useMutation(), no data is sent back to server
In case anybody is interested in using this with the createTRPCProxyClient
object on SvelteKit to expose an /api/trpc/
route... This is how I've done it
// $lib/trpc.ts
import type { AppRouter } from '$lib/server/trpc/routes';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import SuperJSON from 'superjson';
import { svelteQueryWrapper } from 'trpc-svelte-query-adapter';
import type { QueryClient } from '@tanstack/svelte-query';
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:5173/api/trpc'
})
],
transformer: SuperJSON
});
export const trpc = (queryClient?: QueryClient) =>
svelteQueryWrapper<AppRouter>({
client,
queryClient
});
// $lib/server/trpc/index.tsimport { initTRPC } from '@trpc/server';
import type { Context } from '$lib/server/trpc/context';
import superjson from 'superjson';
import { ZodError } from 'zod';
const t = initTRPC.context<Context>().create({
transformer: superjson
});
export const { procedure, router, middleware, createCallerFactory } = t;
// $lib/server/trpc/context.ts
import type { RequestEvent } from '@sveltejs/kit';
import type { inferAsyncReturnType } from '@trpc/server';
export const createContext = async (event: RequestEvent) => ({ event });
export type Context = inferAsyncReturnType<typeof createContext>;
// $lib/server/trpc/caller.ts
import { createCallerFactory } from '$lib/server/trpc';
import { appRouter } from '$lib/server/trpc/routes';
export const createCaller = createCallerFactory(appRouter);
// $lib/server/trpc/routes/index.ts
import { clients } from './clients';
import { files } from './files';
import { projects } from './projects';
import { users } from './users';
import { router } from '$lib/server/trpc';
export const appRouter = router({
users
});
export type AppRouter = typeof appRouter;
// $lib/server/trpc/routes/users.ts
import { isLogged } from '$lib/server/trpc/middlewares/auth';
import { router, procedure } from '$lib/server/trpc';
import { db } from '$lib/server/database/index';
import { client } from '$lib/server/database/schema';
import { eq } from 'drizzle-orm';
import { clientSchema } from '$lib/zod';
export const users = router({
list: procedure
// use query for GET
.query(async () => db.select().from(client)),
delete: procedure
.use(isLogged)
.input(userSchema.pick({ id: true }))
// use mutation for POST
.mutation(async ({ input: { id } }) => {
db.delete(user).where(eq(user.id, id));
})
});
// routes/api/trpc/[...path]/+server.ts
import { createContext } from '$lib/server/trpc/context';
import { appRouter } from '$lib/server/trpc/routes';
import type { RequestHandler } from '@sveltejs/kit';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
const requestHandler: RequestHandler = (event) =>
fetchRequestHandler({
req: event.request,
router: appRouter,
endpoint: '/api/trpc',
createContext: () => createContext(event)
});
export const GET = requestHandler;
export const POST = requestHandler;
// routes/+layout.ts
import { browser } from '$app/environment';
import { QueryClient } from '@tanstack/svelte-query';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
enabled: browser,
staleTime: 60 * 1000
}
}
});
return { queryClient };
};
// routes/+layout.svelte
<script lang="ts">
import { QueryClientProvider } from '@tanstack/svelte-query';
import type { PageData } from './$types';
export let data: PageData;
</script>
<QueryClientProvider client={data.queryClient}>
<main>
<slot />
</main>
</QueryClientProvider>
I am using Lucia so that's why I have the ctx.event.locals.user
and session
in the auth middleware to stop non-logged in users acceessing certain routes.
// $lib/server/trpc/middlewares/auth.ts
import { middleware } from '$lib/server/trpc';
import { TRPCError } from '@trpc/server';
export const isLogged = middleware(async ({ next, ctx }) => {
if (!ctx.event.locals.user && !ctx.event.locals.session)
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authorized!' });
return next();
});
Bonus in case you're interested to see what's in the Zod user schema.
// $lib/zod.ts
import { z } from 'zod';
import validator from 'validator';
export const roles = ['admin', 'user'] as const;
export const roleEnumSchema = z.enum(roles);
export const userSchema = z.object({
id: z.string()
role: roleEnumSchema,
username: z
.string()
.min(3)
.max(31)
.refine((val) => validator.isAlphanumeric(val), {
message: 'Username can only contain letters and numbers.'
}),
password: z.string().min(3).max(31)
});
export type User = z.infer<typeof userSchema>;
This happens due to TRPCQueryOpts
now being required on createQuery
:
trpc-svelte-query-adapter/src/index.ts
Line 207 in 481aa9b
Based on my cursor read, this isn't required since all lookups are of opts?.trpc?.abortOnUnmount
- implying that null values are valid here. Should the type be changed?
Hey. Nice package.
I noticed an error in your documentation,.
export function trpc(queryClient?: QueryClient) {
svelteQueryWrapper<Router>({
client,
queryClient
});
};
You need to return the svelteQueryWrapper
export function trpc(queryClient?: QueryClient) {
return svelteQueryWrapper<Router>({
client,
queryClient
});
};
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.