Skip to content

Commit 05b08c8

Browse files
authored
Store based reactivity (#48)
* feat: add type helper to infer procedure opts type * feat(example): add filter option for todos get query * feat: add support for store input/opts for queries * feat(example): update client-only page to use store as input * feat: add store support for server query input and opts * feat: add store support for server infinite queries * docs: update README note sections * refactor: pull out createQueries and utils procedures into own types * fix: update create(Server)Queries type inference * feat: add store support for create(Server)Queries * deps: update proxy deep * docs: update README to reflect store based reactivity * bump version * docs: fix mistake in README
1 parent d199205 commit 05b08c8

File tree

9 files changed

+573
-381
lines changed

9 files changed

+573
-381
lines changed

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ export const router = t.router({
2020
.then((r) => r[0]);
2121
}),
2222

23-
get: t.procedure.query(({ ctx: { db } }) => db.query.todo.findMany()),
23+
get: t.procedure
24+
.input(z.string().optional())
25+
.query(({ input: filter, ctx: { db } }) =>
26+
db.query.todo.findMany({
27+
where: filter
28+
? (todo, { like }) => like(todo.text, `%${filter}%`)
29+
: undefined,
30+
})
31+
),
2432

2533
getPopular: t.procedure
2634
.input(

@example/src/lib/utils.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function debounce<T>(cb: (v: T) => void, durationMs: number) {
2+
let timer: ReturnType<typeof setTimeout>;
3+
return (v: T) => {
4+
clearTimeout(timer);
5+
timer = setTimeout(() => cb(v), durationMs);
6+
};
7+
}

@example/src/routes/(app)/client-only/+page.svelte

+37-2
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,28 @@
33
44
import { X, Plus } from 'phosphor-svelte';
55
6+
import { debounce } from '$lib/utils';
67
import { page } from '$app/stores';
78
import { trpc } from '$lib/trpc/client';
9+
import { writable } from 'svelte/store';
10+
import type { InferProcedureOpts } from 'trpc-svelte-query-adapter';
811
912
const api = trpc($page);
1013
const utils = api.createUtils();
1114
1215
let todoInput: HTMLInputElement;
1316
14-
const todos = api.todos.get.createQuery();
17+
const filter = writable<string | undefined>();
18+
const opts = writable({
19+
refetchInterval: Infinity,
20+
} satisfies InferProcedureOpts<typeof api.todos.get.createQuery>);
21+
22+
const todos = api.todos.get.createQuery(filter, opts);
1523
1624
const popularTodos = api.todos.getPopular.createInfiniteQuery(
1725
{},
1826
{
19-
getNextPageParam: (data) => data.nextCursor,
27+
getNextPageParam: (lastPage) => lastPage.nextCursor,
2028
}
2129
);
2230
@@ -81,6 +89,33 @@
8189
{/if}
8290
</form>
8391

92+
<form action="#" style="margin-top:0.5rem">
93+
<!-- eslint-disable-next-line svelte/valid-compile -->
94+
<!-- svelte-ignore a11y-no-redundant-roles -->
95+
<fieldset role="group">
96+
<input
97+
type="text"
98+
name="filter"
99+
value={$filter ?? ''}
100+
placeholder="Filter"
101+
on:input|preventDefault={debounce((e) => {
102+
if (!(e.target instanceof HTMLInputElement)) return;
103+
$filter = e.target.value || undefined;
104+
}, 500)}
105+
/>
106+
<input
107+
style="width:15ch;"
108+
type="number"
109+
placeholder="Refetch"
110+
value={$opts?.refetchInterval}
111+
on:input|preventDefault={debounce((e) => {
112+
if (!(e.target instanceof HTMLInputElement)) return;
113+
$opts.refetchInterval = e.target.value ? +e.target.value : Infinity;
114+
}, 500)}
115+
/>
116+
</fieldset>
117+
</form>
118+
84119
<hr />
85120

86121
{#if $todos.isPending}

@example/src/routes/(app)/ssr/+page.svelte

+38-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import Heading from '$lib/components/Heading.svelte';
33
44
import { X, Plus } from 'phosphor-svelte';
5+
import { writable } from 'svelte/store';
6+
import { debounce } from '$lib/utils';
7+
import type { InferProcedureOpts } from 'trpc-svelte-query-adapter';
58
69
export let data;
710
@@ -10,7 +13,13 @@
1013
1114
let todoInput: HTMLInputElement;
1215
13-
const todos = data.todos();
16+
const filter = writable<string | undefined>();
17+
const opts = writable({
18+
refetchInterval: Infinity,
19+
} satisfies InferProcedureOpts<typeof api.todos.get.createQuery>);
20+
21+
const todos = data.todos(filter, opts);
22+
1423
const popularTodos = data.popularTodos();
1524
1625
const createTodo = api.todos.create.createMutation({
@@ -31,7 +40,7 @@
3140
});
3241
</script>
3342

34-
<Heading>Client-only</Heading>
43+
<Heading>SSR</Heading>
3544

3645
<div id="content" style="margin-top:2rem">
3746
<div>
@@ -74,6 +83,33 @@
7483
{/if}
7584
</form>
7685

86+
<form action="#" style="margin-top:0.5rem">
87+
<!-- eslint-disable-next-line svelte/valid-compile -->
88+
<!-- svelte-ignore a11y-no-redundant-roles -->
89+
<fieldset role="group">
90+
<input
91+
type="text"
92+
name="filter"
93+
value={$filter ?? ''}
94+
placeholder="Filter"
95+
on:input|preventDefault={debounce((e) => {
96+
if (!(e.target instanceof HTMLInputElement)) return;
97+
$filter = e.target.value || undefined;
98+
}, 500)}
99+
/>
100+
<input
101+
style="width:15ch;"
102+
type="number"
103+
placeholder="Refetch"
104+
value={$opts?.refetchInterval}
105+
on:input|preventDefault={debounce((e) => {
106+
if (!(e.target instanceof HTMLInputElement)) return;
107+
$opts.refetchInterval = e.target.value ? +e.target.value : Infinity;
108+
}, 500)}
109+
/>
110+
</fieldset>
111+
</form>
112+
77113
<hr />
78114

79115
{#if $todos.isPending}

@example/src/routes/(app)/ssr/+page.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export async function load(event) {
1111
popularTodos: await api.todos.getPopular.createServerInfiniteQuery(
1212
{},
1313
{
14-
getNextPageParam: (data) => data.nextCursor,
14+
getNextPageParam: (lastPage) => lastPage.nextCursor,
1515
}
1616
),
1717
};

@lib/README.md

+46-39
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
[![License][license-image]][license-url]
55
[![Last commit][last-commit-image]][repo-url]
66

7-
> **NOTE:** The README on [npmjs](https://npmjs.com/trpc-svelte-query-adapter) might not be fully up to date. Please refer to the [README on the Github Repo](https://github.com/vishalbalaji/trpc-svelte-query-adapter/#readme) for the latest setup instructions.
7+
> [!NOTE]
8+
> The README on [npmjs](https://npmjs.com/trpc-svelte-query-adapter) might not be fully up to date. Please refer to
9+
> the [README on the Github Repo](https://github.com/vishalbalaji/trpc-svelte-query-adapter/#readme) for the latest setup instructions.
810
9-
This package provides an adapter to call `tRPC` procedures wrapped with <code>[@tanstack/svelte-query](https://tanstack.com/query/latest/docs/svelte/overview)</code>, similar to <code>[@trpc/react-query](https://trpc.io/docs/react-query)</code>. This is made possible using <code>[proxy-deep](https://www.npmjs.com/package/proxy-deep)</code>.
11+
An adapter to call `tRPC` procedures wrapped with <code>[@tanstack/svelte-query](https://tanstack.com/query/latest/docs/svelte/overview)</code>, similar to <code>[@trpc/react-query](https://trpc.io/docs/react-query)</code>. This is made possible using <code>[proxy-deep](https://www.npmjs.com/package/proxy-deep)</code>.
1012

1113
## Installation
1214

13-
```console
15+
```sh
1416
# npm
1517
npm install trpc-svelte-query-adapter @trpc/client @trpc/server @tanstack/svelte-query
1618

@@ -202,7 +204,8 @@ By default, these 3 procedures will pre-fetch the data required to pre-render th
202204

203205
These procedures can be used as such:
204206

205-
> **NOTE:** [Gotta await top-level promises to pre-fetch data from SvelteKit v2](https://kit.svelte.dev/docs/migrating-to-sveltekit-2#top-level-promises-are-no-longer-awaited).
207+
> [!NOTE]
208+
> [Gotta await top-level promises to pre-fetch data from SvelteKit v2](https://kit.svelte.dev/docs/migrating-to-sveltekit-2#top-level-promises-are-no-longer-awaited).
206209
207210
```typescript
208211
// +page.ts
@@ -258,58 +261,62 @@ Then, in the component:
258261
{/each}
259262
```
260263

261-
You can also optionally pass new inputs to the queries and infinite queries from the client side(see [#34](/../../issues/34)) like so:
264+
You can also optionally pass new inputs to the queries and infinite queries from the client side(see [#34](/../../issues/34), [#47]( /../../issues/47 )) like so:
262265

263266
```svelte
264267
<script lang="ts">
265268
import { page } from "$app/stores";
266269
import type { PageData } from "./$types";
267270
271+
import { derived, writable } from '@svelte/store';
272+
268273
export let data: PageData;
269274
270-
let name = 'foo';
271-
let newNames: string[] = [];
275+
const name = writable('foo');
276+
const newNames = writable<string[]>([]);
272277
273-
$: foo = data.foo(name); // `$` label to make the query reactive to the input
278+
const foo = data.foo($name);
274279
275280
// You can also access the default input if you pass in a callback as the new input:
276-
// $: foo = data.foo((old) => old + name);
281+
// const foo = data.foo((old) => derived(name, ($name) => old + name));
277282
278-
$: queries = data.queries((t, old) => [...old, ...newNames.map((name) => t.greeting(name))]);
283+
const queries = data.queries((t, old) => derived(newNames, ($newNames) => [...old, ...$newNames.map((name) => t.greeting(name))]));
279284
</script>
280285
281-
{#if $foo.isPending}
282-
Loading...
283-
{:else if $foo.isError}
284-
{$foo.error}
285-
{:else if $foo.data}
286-
{$foo.data}
287-
{/if}
288-
289-
<br />
290-
<input bind:value={name} />
291-
292-
<br /><br />
293-
294-
{#each $queries as query}
295-
{#if query.isPending}
286+
<div>
287+
{#if $foo.isPending}
296288
Loading...
297-
{:else if query.isError}
298-
{query.error.message}
299-
{:else if query.data}
300-
{query.data}
289+
{:else if $foo.isError}
290+
{$foo.error}
291+
{:else if $foo.data}
292+
{$foo.data}
301293
{/if}
302-
<br />
303-
{/each}
294+
<input bind:value={$name} />
295+
</div>
296+
297+
<br />
304298
305-
<form on:submit|preventDefault={(e) => {
306-
const data = new FormData(e.currentTarget).get('name');
307-
if (typeof data === 'string') newNames.push(data);
308-
newNames = newNames;
309-
}}>
310-
<input name="name" />
311-
<button type="submit">Submit</button>
312-
</form>
299+
<div>
300+
{#each $queries as query}
301+
{#if query.isPending}
302+
Loading...
303+
{:else if query.isError}
304+
{query.error.message}
305+
{:else if query.data}
306+
{query.data}
307+
{/if}
308+
<br />
309+
{/each}
310+
311+
<form on:submit|preventDefault={(e) => {
312+
const data = new FormData(e.currentTarget).get('name');
313+
if (typeof data === 'string') $newNames.push(data);
314+
$newNames = $newNames;
315+
}}>
316+
<input name="name" />
317+
<button type="submit">Submit</button>
318+
</form>
319+
</div>
313320
```
314321

315322
[npm-url]: https://npmjs.org/package/trpc-svelte-query-adapter

@lib/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "trpc-svelte-query-adapter",
3-
"version": "2.3.7",
3+
"version": "2.3.8",
44
"description": "A simple adapter to use `@tanstack/svelte-query` with trpc, similar to `@trpc/react-query`.",
55
"keywords": [
66
"trpc",
@@ -34,7 +34,7 @@
3434
"import": "./src/index.ts"
3535
},
3636
"dependencies": {
37-
"proxy-deep": "^3.1.1"
37+
"proxy-deep": "^4.0.1"
3838
},
3939
"peerDependencies": {
4040
"@tanstack/svelte-query": "^5.8.2",

0 commit comments

Comments
 (0)