Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to stream queries to Pages/Components? #22

Open
dihmeetree opened this issue Feb 6, 2024 · 8 comments
Open

How to stream queries to Pages/Components? #22

dihmeetree opened this issue Feb 6, 2024 · 8 comments

Comments

@dihmeetree
Copy link

dihmeetree commented Feb 6, 2024

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!

@vishalbalaji
Copy link
Owner

vishalbalaji commented Feb 9, 2024

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!

@dihmeetree
Copy link
Author

dihmeetree commented Feb 10, 2024

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 😢

@vishalbalaji
Copy link
Owner

vishalbalaji commented Feb 11, 2024

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

@vishalbalaji
Copy link
Owner

Hey @dihmeetree, closing this issue due to inactivity. Hope that your query was resolved.

@dihmeetree
Copy link
Author

dihmeetree commented Apr 11, 2024

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

@vishalbalaji
Copy link
Owner

vishalbalaji commented Apr 14, 2024

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.

@vishalbalaji vishalbalaji reopened this Apr 14, 2024
@natedunn
Copy link

natedunn commented Jun 17, 2024

I am awaiting (😏) 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. 😞

@vishalbalaji
Copy link
Owner

vishalbalaji commented Jun 18, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants