Skip to content

Commit 93b2ee3

Browse files
authored
Lazy query initialization for createQuery and createInfiniteQuery (#52)
* refactor: clean up query opts type definitions * refactor: remove NoInfer from queries to allow inference for `select` * feat: add lazy option to `createQuery` * feat: add support for lazy infinite queries * bump version * feat(example): add `ssr-with-streaming` to index page
1 parent 7167aba commit 93b2ee3

File tree

6 files changed

+419
-100
lines changed

6 files changed

+419
-100
lines changed

@example/src/lib/trpc/router.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import type { Context } from '$lib/trpc/context';
1+
import { createContext, type Context } from '$lib/trpc/context';
22
import { initTRPC } from '@trpc/server';
33
import { z } from 'zod';
44

55
import { todo } from '$lib/server/db/schema';
66
import { eq } from 'drizzle-orm';
7+
import type { RequestEvent } from '../../routes/(app)/ssr2/$types';
78

89
export const t = initTRPC.context<Context>().create();
910

@@ -75,6 +76,9 @@ export const router = t.router({
7576
}),
7677
});
7778

78-
export const createCaller = t.createCallerFactory(router);
79+
const factory = t.createCallerFactory(router);
80+
export const createCaller = async (event: RequestEvent) => {
81+
return factory(await createContext(event));
82+
};
7983

8084
export type Router = typeof router;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createCaller } from '$lib/trpc/router';
2+
3+
export async function load(event) {
4+
const api = await createCaller(event);
5+
return {
6+
popularTodos: api.todos.getPopular({}),
7+
todos: await api.todos.get(),
8+
};
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
<script lang="ts">
2+
import Heading from '$lib/components/Heading.svelte';
3+
4+
import { trpc } from '$lib/trpc/client.js';
5+
import { page } from '$app/stores';
6+
7+
import { X, Plus } from 'phosphor-svelte';
8+
import { writable } from 'svelte/store';
9+
import { debounce } from '$lib/utils';
10+
11+
export let data;
12+
13+
const api = trpc($page);
14+
const utils = api.createUtils();
15+
16+
let todoInput: HTMLInputElement;
17+
18+
const filter = writable<string | undefined>();
19+
const opts = writable(
20+
api.todos.get.createQuery.opts({
21+
initialData: data.todos,
22+
refetchInterval: Infinity,
23+
})
24+
);
25+
26+
const todos = api.todos.get.createQuery(filter, opts);
27+
28+
const [popularTodos, resolvePopularTodos] =
29+
api.todos.getPopular.createInfiniteQuery(
30+
{},
31+
{
32+
getNextPageParam: (lastPage) => lastPage.nextCursor,
33+
lazy: true,
34+
}
35+
);
36+
37+
const createTodo = api.todos.create.createMutation({
38+
onSuccess() {
39+
utils.todos.get.invalidate();
40+
todoInput.value = '';
41+
},
42+
});
43+
const deleteTodo = api.todos.delete.createMutation({
44+
onSuccess: () => {
45+
utils.todos.get.invalidate();
46+
},
47+
});
48+
const updateTodo = api.todos.update.createMutation({
49+
onSuccess: () => {
50+
utils.todos.get.invalidate();
51+
},
52+
});
53+
</script>
54+
55+
<Heading>SSR</Heading>
56+
57+
<div id="content" style="margin-top:2rem">
58+
<div>
59+
<h2>Todos</h2>
60+
61+
<form
62+
action="#"
63+
on:submit|preventDefault={async (e) => {
64+
// @ts-expect-error - ??
65+
const { text } = e.currentTarget.elements;
66+
$createTodo.mutate(text.value);
67+
}}
68+
>
69+
<!-- eslint-disable-next-line svelte/valid-compile -->
70+
<!-- svelte-ignore a11y-no-redundant-roles -->
71+
<fieldset role="group" style="margin: 0">
72+
<input
73+
bind:this={todoInput}
74+
placeholder="Ex: Do shopping"
75+
aria-invalid={$createTodo.isError || undefined}
76+
disabled={$todos.isPending || $createTodo.isPending}
77+
name="text"
78+
type="text"
79+
/>
80+
<input
81+
disabled={$todos.isPending || $createTodo.isPending}
82+
type="submit"
83+
value="Create Todo"
84+
/>
85+
</fieldset>
86+
87+
{#if $createTodo.isError}
88+
<div style="margin-top:0.5rem">
89+
{#each JSON.parse($createTodo.error.message) as error}
90+
<span style="color:var(--pico-color-red-450)">
91+
Error: {error.message}
92+
</span>
93+
{/each}
94+
</div>
95+
{/if}
96+
</form>
97+
98+
<form action="#" style="margin-top:0.5rem">
99+
<!-- eslint-disable-next-line svelte/valid-compile -->
100+
<!-- svelte-ignore a11y-no-redundant-roles -->
101+
<fieldset role="group">
102+
<input
103+
type="text"
104+
name="filter"
105+
value={$filter ?? ''}
106+
placeholder="Filter"
107+
on:input|preventDefault={debounce((e) => {
108+
if (!(e.target instanceof HTMLInputElement)) return;
109+
$filter = e.target.value || undefined;
110+
}, 500)}
111+
/>
112+
<input
113+
style="width:15ch;"
114+
type="number"
115+
placeholder="Refetch"
116+
value={$opts?.refetchInterval}
117+
on:input|preventDefault={debounce((e) => {
118+
if (!(e.target instanceof HTMLInputElement)) return;
119+
$opts.refetchInterval = e.target.value ? +e.target.value : Infinity;
120+
}, 500)}
121+
/>
122+
</fieldset>
123+
</form>
124+
125+
<hr />
126+
127+
{#if $todos.isPending}
128+
<article>
129+
<progress />
130+
Loading todos...
131+
</article>
132+
{:else if $todos.isError}
133+
<article>
134+
Error loading todos: {$todos.error}
135+
</article>
136+
{:else if $todos.data.length <= 0}
137+
<article style="text-align:center">Create a new Todo!</article>
138+
{:else}
139+
<div style="max-height:70vh;overflow:hidden;overflow-y:auto">
140+
{#each $todos.data as todo}
141+
<article style="display:flex;align-items:center;gap:0.5rem;">
142+
<input
143+
title={`Mark as ${todo.done ? 'Not Done' : 'Done'}`}
144+
type="checkbox"
145+
disabled={$todos.isPending || $createTodo.isPending}
146+
checked={todo.done}
147+
on:change|preventDefault={() => {
148+
$updateTodo.mutate({ id: todo.id, done: !todo.done });
149+
}}
150+
/>
151+
152+
<span>
153+
{#if todo.done}
154+
<s>{todo.text}</s>
155+
{:else}
156+
{todo.text}
157+
{/if}
158+
</span>
159+
160+
<button
161+
on:click|preventDefault={() => {
162+
$deleteTodo.mutate(todo.id);
163+
}}
164+
title="Delete Todo"
165+
disabled={$todos.isPending || $createTodo.isPending}
166+
class="outline contrast pico-color-red-450"
167+
style="margin-left:auto;padding:0.1rem;line-height:1;border-color:var(--pico-color-red-450);display:grid;place-items:center;"
168+
>
169+
<X />
170+
</button>
171+
</article>
172+
{/each}
173+
</div>
174+
{/if}
175+
{#if $createTodo.isPending || $deleteTodo.isPending || $updateTodo.isPending}
176+
<progress />
177+
{/if}
178+
</div>
179+
180+
<div>
181+
<h2>Popular Todos (from JSONPlaceholder API)</h2>
182+
<button
183+
on:click|preventDefault={() => $popularTodos.fetchNextPage()}
184+
class="outline"
185+
style="display:block;margin-left:auto"
186+
>
187+
Fetch more
188+
</button>
189+
<hr />
190+
191+
{#await resolvePopularTodos(data.popularTodos)}
192+
<article>
193+
<progress />
194+
Streaming popular todos...
195+
</article>
196+
{:then}
197+
{#if $popularTodos.isPending || $popularTodos.isFetching}
198+
<article>
199+
<progress />
200+
Loading popular todos...
201+
</article>
202+
{:else if $popularTodos.isError}
203+
<article>
204+
Error loading todos: {$popularTodos.error}
205+
</article>
206+
{:else if $popularTodos.data}
207+
<div style="max-height:70vh;overflow:hidden;overflow-y:auto">
208+
{#each $popularTodos.data?.pages.flatMap((page) => page.todos) as todo}
209+
<article
210+
style="display:flex;align-items:center;justify-content:space-between;"
211+
>
212+
<span>
213+
{todo.id}: {todo.title}
214+
</span>
215+
216+
<button
217+
on:click|preventDefault={() => {
218+
$createTodo.mutate(todo.title);
219+
}}
220+
title="Add Todo"
221+
disabled={$popularTodos.isPending || $createTodo.isPending}
222+
class="outline contrast pico-color-green-450"
223+
style="margin-left:auto;padding:0.1rem;line-height:1;border-color:var(--pico-color-green-450);display:grid;place-items:center;"
224+
>
225+
<Plus />
226+
</button>
227+
</article>
228+
{/each}
229+
</div>
230+
{/if}
231+
{/await}
232+
</div>
233+
</div>
234+
235+
<style>
236+
#content {
237+
display: grid;
238+
grid-template-columns: 1fr;
239+
gap: 1rem;
240+
}
241+
242+
@media (min-width: 1024px) {
243+
#content {
244+
grid-template-columns: 1fr 1fr;
245+
}
246+
}
247+
</style>

@example/src/routes/+page.svelte

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@
44

55
<div style="display:flex;flex-direction:column;gap:2rem;">
66
<div style="display:flex;align-items:center;justify-content:space-between;">
7-
<Heading prefix={false}>
8-
tRPC - Svelte-Query Adapter Demo
9-
</Heading>
7+
<Heading prefix={false}>tRPC - Svelte-Query Adapter Demo</Heading>
108
</div>
119

1210
<div>
1311
<h2>Examples</h2>
1412
<ul>
1513
<li><a href="/client-only">Client-only</a></li>
1614
<li><a href="/ssr">SSR</a></li>
15+
<li><a href="/ssr-with-streaming">SSR with Streaming</a></li>
1716
</ul>
1817
</div>
1918
</div>

@lib/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "trpc-svelte-query-adapter",
3-
"version": "2.3.10",
3+
"version": "2.3.11",
44
"description": "A simple adapter to use `@tanstack/svelte-query` with trpc, similar to `@trpc/react-query`.",
55
"keywords": [
66
"trpc",

0 commit comments

Comments
 (0)