Closed dihmeetree closed 2 months ago
Hey @dihmeetree, to stream stuff from the load function in Sveltekit, you would need to return it as a nested property, something like so:
// +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 {
streamed: { // Doesn't have to be named 'streamed'
queries: await api.createServerQueries((t) => [
t.authed.todos.all(),
t.public.hello.get()
])
}
};
}
https://svelte.dev/blog/streaming-snapshots-sveltekit#stream-non-essential-data-in-load-functions
I've never tried to do it with queries before and am honestly not really sure if it will work as intended. Sounds really interesting, though.
I hope that this is helpful, give it a try and please update me if it works!
Hmmm @vishalbalaji, wouldn't queries
need to be returned as a promise? Look's like you're awaiting the api.createServerQueries
function, which I don't think would work. Also not really sure how the queries would work as a promise on the front end tbh 😢
@dihmeetree You are right about the fact that you can't await queries, because the queries themselves are essentially stores and Svelte stores can only be created at the top level. However, one thing to note here is that createServerQuery
is mainly just a wrapper around createQuery
that calls queryClient.prefetchQuery
and returns the query wrapped in a function, which SvelteKit's load
function can serialize.
This means that you should be able to achieve what you are trying to do by streaming the prefetch
call and creating the query manually in the component, somewhat like so:
// src/routes/+page.ts
import { trpc } from "$lib/trpc/client";
import type { PageLoadEvent } from "./$types";
export async function load(event: PageLoadEvent) {
const { queryClient } = await event.parent();
const api = trpc(event, queryClient);
const utils = api.createUtils();
return {
// This query has to be disabled to prevent data being fetched again
// from the client side.
foo: () => api.greeting.createQuery('foo', { enabled: false }),
bar: await api.greeting.createServerQuery('bar'),
nested: {
foo: (async () => {
await new Promise((r) => setTimeout(r, 2000)); // delay to simulate a network response
await utils.greeting.prefetch('foo');
})(),
}
}
}
<!-- src/routes/+page.svelte -->
<script lang="ts">
export let data;
const foo = data.foo();
const bar = data.bar();
</script>
<p>
{#if $bar.isPending}
Loading...
{:else if $bar.isError}
Error: {$bar.error.message}
{:else}
{$bar.data}
{/if}
</p>
{#await data.nested.foo}
<!-- -->
{/await}
<p>
{#if $foo.isPending}
Streaming...
{:else if $foo.isError}
Error: {$foo.error.message}
{:else}
{$foo.data}
{/if}
</p>
<button on:click={() => $foo.refetch()}>Refetch foo</button>
I am demonstrating here using createQuery
, but the prefetching would apply to each query in the server queries as well.
Here's a StackBlitz reproduction to see this in action: https://stackblitz.com/edit/stackblitz-starters-dvtn9s?file=src%2Froutes%2F%2Bpage.ts
Maybe this could be simplified by creating a separate abstraction around this, called createStreamedQuery
, which will return an array with [query, prefetchQuery]
. I envision this being used like so:
export async function load(event: PageLoadEvent) {
const { queryClient } = await event.parent();
const api = trpc(event, queryClient);
const [comments, prefetchComments] = createStreamedQuery(...);
return {
api,
comments,
nested: {
loadComments: await prefetchComments(),
}
}
}
And in the template:
<script lang="ts">
export let data;
const comments = data.comments();
</script>
{#await data.nested.loadComments}
Loading comments...
{:then}
<p>
{#if $comments.isLoading || $comments.isFetching}
Loading...
{:else if $comments.isError}
Error: {$comments.error.message}
{:else}
{$comments.data}
{/if}
</p>
<button on:click={() => $comments.refetch()}>Refetch comments</button>
{/await}
Let me know if this is something that you see being useful in the library, but I think the above solution should be enough to resolve this issue.
Anyway, thanks a lot for bringing this to my attention, this is quite interesting and I hadn't thought this far when I was initially working on this library.
Hey @dihmeetree, closing this issue due to inactivity. Hope that your query was resolved.
@vishalbalaji can we re-open this topic if you don't mind? I'm re-exploring TRPC and this package again. Apologies for not responding back to you.
Regarding your last post..your example unfortunately doesn't do what I'm looking for. Streaming involves the use of await
in the .svelte
page (i.e - https://svelte.dev/blog/streaming-snapshots-sveltekit). In your example, it looks like the foo
query still runs on the client (after the timeout). This query should be fetched on the server only. Any refetches could be done on the client from there on out.
So the server load function should return a promise of the query and then the frontend uses await to stream the data to the page.
Hi @dihmeetree, I've been looking into this for the past couple of days now and found out that the client request seems to be occurring due to the tRPC request itself, as demonstrated with this example where I am trying to construct this scenario manually using the tRPC request and tanstack query:
// src/routes/+page.ts
import { trpc } from "$lib/trpc/client";
import type { PageLoadEvent } from "./$types";
import { createQuery } from "@tanstack/svelte-query";
async function foo(event: PageLoadEvent) {
await new Promise((r) => setTimeout(r, 2000)); // delay to simulate a network response
// non-trpc request
// client request is not made
return 'foo';
// trpc-request
// client request is made
// const client = trpc(event)
// return client.greeting.query('foo');
}
export async function load(event: PageLoadEvent) {
const { queryClient } = await event.parent();
const fooQuery = { queryKey: ['foo'], queryFn: () => foo(event) };
return {
foo: () => createQuery(fooQuery),
streamedFoo: queryClient.prefetchQuery(fooQuery),
}
}
<!-- src/routes/+page.svelte -->
<script lang="ts">
export let data;
const foo = data.foo();
</script>
<p>
{#await data.streamedFoo}
Streaming...
{:then}
{#if $foo.isPending || $foo.isLoading || $foo.isFetching}
Loading...
{:else if $foo.isError}
Error: {$foo.error.message}
{:else}
{$foo.data}
{/if}
{/await}
</p>
<button on:click={() => $foo.refetch()}>Refetch foo</button>
Seems like it might be occurring due to the fact that the trpc call is happening on both client and server since we are calling it in +page.ts
instead of +page.server.ts
, which would involve creating a server caller, like detailed here: https://trpc.io/docs/server/server-side-calls.
Let me re-open this issue until I figure out a solution for this.
I am await
ing (😏) any good ideas on this. Any further suggestions? I thought of using the server caller instead but I'd miss out on the query features that we use this library for. 😞
Hey @natedunn, sorry for the long standing issue. I haven't been able to look into this issue much because I've been a bit busy with work and other things the past few weeks.
From what I have seen though, the problem here seems to be an issue with tRPC
itself. For some reason, the tRPC
client doesn't dedup requests made on the server even if we use the fetch
we get from the load function.
For example, this doesn't work as expected:
// +page.ts
import type { PageLoad } from "./$types";
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import type { Router } from "$lib/trpc/router";
export const load = (async (event) => {
const api = createTRPCProxyClient<Router>({
links: [httpBatchLink({
fetch: event.fetch,
url: `${event.url.origin}/trpc`,
})]
});
return {
waitFoo: (async () => {
await new Promise((r) => setTimeout(r, 2000));
return api.greeting.query('foo');
})()
};
}) satisfies PageLoad;
<script lang="ts">
import type { PageData } from "./$types";
export let data: PageData;
</script>
{#await data.waitFoo}
Awaiting...
{:then foo}
{foo}
{/await}
The client side request doesn't happen when doing the same with a regular API endpoint. If anyone has any insight into this, I would love to know why.
Hey all, just thought that I would post an update to this issue since its been open for way longer that it should have been. I recently got some spare time to look into this a bit more and here's what I have gathered so far.
First off, I came upon the realization that you can't really stream from a page.ts
file like I had suggested in earlier comments. You can only stream requests from +page.server.ts
. I could have sworn that I had streamed data from a +page.ts
file before, but I might have just misunderstood something.
With this in mind, the steps to stream query data from the server become a bit simpler:
+page.server.ts
file to either fetch or stream requests.+page.svelte
file, with the enabled
property set to false
.This, in implementation might look something like this:
// $lib/trpc/router.ts
import type { Context } from '$lib/trpc/context';
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
export const t = initTRPC.context<Context>().create();
export const router = t.router({
getPosts: t.procedure.input(z.number()).query(async ({ input: id }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
return res.json();
}),
getComments: t.procedure.input(z.number()).query(async ({ input: id }) => {
await new Promise((r) => setTimeout(r, 5000)); // 5 second simulated delay
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}/comments`);
return res.json();
})
});
export const createCaller = t.createCallerFactory(router);
export type Router = typeof router;
// +page.server.ts
import { createContext } from '$lib/trpc/context';
import { createCaller } from '$lib/trpc/router';
export async function load(event) {
const api = createCaller(await createContext(event));
return {
comments: api.getComments(1),
post: await api.getPost(1)
};
}
<!-- +page.svelte -->
<script lang="ts">
import { page } from '$app/stores';
import { trpc } from '$lib/trpc/client';
import type { PageData } from './$types';
export let data: PageData;
const queryClient = data.queryClient;
const api = trpc($page);
const post = api.getPost.createQuery(1, { initialData: data.post });
let enabled = false;
$: comments = api.getComments.createQuery(1, { enabled });
async function resolveQuery(key: QueryKey, promise: Promise<unknown>) {
queryClient.setQueryData(api.getComments.getQueryKey(1, 'query'), await promise);
enabled = true;
}
</script>
<h1>{$post.data?.title}</h1>
<p>{$post.data?.body}</p>
<h2>Comments</h2>
{#await resolveQuery(data.comments)}
Loading comments...
{:then}
{#each $comments.data as comment}
<p><b>{comment.email}</b>: {comment.body}</p>
{/each}
{/await}
This in its raw form is a bit janky, especially if this needs to be repeated for multiple queries, but this can be done.
Now, I was thinking of modifying the implementation to have an abstracted version of this in the library. However, I took a look at the updated docs for svelte-query
v5 and realized that they have changed the way on how reactive queries are to be done and are recommending using stores instead of the $
label. I feel that it would be better moving forward if I fixed the library to support that first and might be implementing support for store opts, potentially in the next few days.
But if you want to have streamed queries in the meantime, the above example still works as expected.
Instead of doing the following (to prerender queries on the server):
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 theawait
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):
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!