@zenstackhq/tanstack-query
If you're looking for generating hooks for SWR, please checkout the @zenstackhq/swr
plugin.
The @zenstackhq/tanstack-query
plugin generates Tanstack Query hooks that call into the CRUD services provided by the server adapters. The plugin currently supports React and Svelte. Vue support is coming soon.
The hooks syntactically mirror the APIs of a standard Prisma client, including the function names and shapes of parameters (hooks directly use types generated by Prisma).
To use the generated hooks, you need to install "tanstack-query" for the target framework with version 4.0.0 or above.
Options​
Name | Type | Description | Required | Default |
---|---|---|---|---|
output | String | Output directory (relative to the path of ZModel) | Yes | |
target | String | Target framework to generate for. Choose from "react" and "svelte". | Yes | |
useSuperJson | Boolean | Use superjson for data serialization | No | false |
Context Provider​
The plugin generates a context provider which you can use to configure the behavior of the hooks. The following options are available on the provider:
endpoint
The endpoint to use for the queries. Defaults to "/api/model".
fetch
A custom
fetch
function to use for the queries. Defaults to the browser's built-infetch
.
Example for using the context provider:
- React
- Svelte
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { FetchFn, Provider as ZenStackHooksProvider } from '../lib/hooks';
// custom fetch function that adds a custom header
const myFetch: FetchFn = (url, options) => {
options = options ?? {};
options.headers = {
...options.headers,
'x-my-custom-header': 'hello world',
};
return fetch(url, options);
};
const queryClient = new QueryClient();
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return (
<QueryClientProvider client={queryClient}>
<ZenStackHooksProvider value={{ endpoint: '/api/model', fetch: myFetch }}>
<AppContent />
</ZenStackHooksProvider>
</QueryClientProvider>
);
}
export default MyApp;
<script lang="ts">
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
import { SvelteQueryContextKey, type FetchFn } from '$lib/hooks';
import { setContext } from 'svelte';
// custom fetch function that adds a custom header
const myFetch: FetchFn = (url, options) => {
options = options ?? {};
options.headers = {
...options.headers,
'x-my-custom-header': 'hello world',
};
return fetch(url, options);
};
setContext(SvelteQueryContextKey, {
endpoint: '/api/model',
fetch: myFetch,
});
const queryClient = new QueryClient();
</script>
<div>
<QueryClientProvider client={queryClient}>
<slot />
</QueryClientProvider>
</div>
Example​
Here's a quick example with a ReactJs blogging app. You can find a fully functional Todo app example here.
Schema​
plugin hooks {
provider = '@zenstackhq/tanstack-query'
output = "./src/lib/hooks"
target = "react"
}
model User {
id String @id @default(cuid())
email String
posts Post[]
// everyone can signup, and user profile is also publicly readable
@@allow('create,read', true)
}
model Post {
id String @id @default(cuid())
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
// author has full access
@@allow('all', auth() == author)
// logged-in users can view published posts
@@allow('read', auth() != null && published)
}
App Setup​
- React
- Svelte
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Provider as ZenStackHooksProvider } from '../lib/hooks';
const queryClient = new QueryClient();
function App({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return (
<QueryClientProvider client={queryClient}>
<ZenStackHooksProvider value={{ endpoint: '/api/model' }}>
<AppContent />
</ZenStackHooksProvider>
</QueryClientProvider>
);
}
<script lang="ts">
import { SvelteQueryContextKey } from '$lib/hooks';
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
import { setContext } from 'svelte';
const queryClient = new QueryClient();
setContext(SvelteQueryContextKey, { endpoint: '/api/model' });
</script>
<div>
<QueryClientProvider client={queryClient}>
<slot />
</QueryClientProvider>
</div>
If you're using SvelteKit, you need to make sure the package @zenstackhq/tanstack-query
is not treated as an external dependency by Vite, otherwise you'll encounter errors when the SvelteKit server renders the app. To do this, add the following section into vite.config.js
:
const config = {
...
ssr: {
noExternal: ['@zenstackhq/tanstack-query'],
},
};
export default config;
Using Query and Mutation Hooks​
- React
- Svelte
import type { Post } from '@prisma/client';
import { useFindManyPost, useCreatePost } from '../lib/hooks';
// post list component
const Posts = ({ userId }: { userId: string }) => {
const create = useCreatePost();
// list all posts that're visible to the current user, together with their authors
const { data: posts } = useFindManyPost({
include: { author: true },
orderBy: { createdAt: 'desc' },
});
async function onCreatePost() {
create.mutate({
data: {
title: 'My awesome post',
authorId: userId,
},
});
}
return (
<>
<button onClick={onCreatePost}>Create</button>
<ul>
{posts?.map((post) => (
<li key={post.id}>
{post.title} by {post.author.email}
</li>
))}
</ul>
</>
);
};
<script lang="ts">
import type { Post } from '@prisma/client';
import { useFindManyPost, useCreatePost } from '../lib/hooks';
export let userId: string;
const query = useFindManyPost();
const create = useCreatePost();
function onCreatePost() {
$create.mutate({
data: {
title: 'My awesome post',
authorId: userId,
},
});
}
</script>
<div>
<button on:click={onCreatePost}>Create</button>
<ul>
{#each $query.data as post (post.id)}
<li>{post.title} by {post.author.email}</li>
{/each}
</ul>
</div>
Using Infinite Query​
- React
- Svelte
See Tanstack Query documentation for more details.
import type { Post } from '@prisma/client';
import { useInfiniteFindManyPost } from '../lib/hooks';
// post list component with infinite loading
const Posts = () => {
const PAGE_SIZE = 10;
const fetchArgs = {
include: { author: true },
orderBy: { createdAt: 'desc' as const },
take: PAGE_SIZE,
};
const { data, fetchNextPage, hasNextPage } = useInfiniteFindManyPost(fetchArgs, {
getNextPageParam: (lastPage, pages) => {
if (lastPage.length < PAGE_SIZE) {
return undefined;
}
const fetched = pages.flatMap((item) => item).length;
return {
...fetchArgs,
skip: fetched,
};
},
});
return (
<>
<ul>
{data?.pages.map((posts, i) => (
<React.Fragment key={i}>
{posts?.map((post) => (
<li key={post.id}>
{post.title} by {post.author.email}
</li>
))}
</React.Fragment>
))}
</ul>
{hasNextPage && (
<button onClick={() => fetchNextPage()}>
Load more
</button>
)}
</>
);
};
See Tanstack Query documentation for more details.
<script lang="ts">
// post list component with infinite loading
import type { Post } from '@prisma/client';
import { useInfiniteFindManyPost } from '../lib/hooks';
const PAGE_SIZE = 10;
const fetchArgs = {
include: { author: true },
orderBy: { createdAt: 'desc' as const },
take: PAGE_SIZE,
};
const query = useInfiniteFindManyPost(fetchArgs, {
getNextPageParam: (lastPage, pages) => {
if (lastPage.length < PAGE_SIZE) {
return undefined;
}
const fetched = pages.flatMap((item) => item).length;
return {
...fetchArgs,
skip: fetched,
};
},
});
</script>
<div>
<ul>
<div>
{#if $query.data}
{#each $query.data.pages as posts, i (i)}
{#each posts as post (post.id)}
<li>{post.title} by {post.author.email}</li>
{/each}
{/each}
{/if}
</div>
</ul>
{#if $query.hasNextPage}
<button on:click={() => $query.fetchNextPage()}>
Load more
</button>
{/if}
</div>