| sidebar_position | 1 |
|---|
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import PackageInstall from '../../../_components/PackageInstall'; import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; import OptimisticBehavior from './_optimistic-behavior.md'; import OptimisticLimitation from './_optimistic-limitation.md'; import FineGrainedOptimistic from './_fine-grained-optimistic.md'; import Invalidation from './_invalidation.md'; import PreviewFeature from '../../../_components/PreviewFeature.tsx'
TanStack Query is a powerful data-fetching library for the web frontend, supporting multiple UI frameworks like React, Vue, and Svelte.
:::info TanStack Query integration only works with the RPC style API. Currently supported frameworks are: react, vue, and svelte.
This documentation assumes you have a solid understanding of TanStack Query concepts. :::
ZenStack's TanStack Query integration helps you derive a set of fully typed query and mutation hooks from your data model. The derived hooks work with the RPC style API and pretty much 1:1 mirror the ORM query APIs, allowing you to enjoy the same excellent data query DX from the frontend.
The integration provides the following features
- Query and mutation hooks like
useFindMany,useUpdate, etc. - All hooks accept standard TanStack Query options, allowing you to customize their behavior.
- Standard, infinite, and suspense queries.
- Automatic query invalidation upon mutation.
- Automatic optimistic updates (opt-in).
@tanstack/react-query: v5+reactv18+
@tanstack/vue-query: v5+vuev3+
@tanstack/svelte-query: v6+sveltev5.25.0+
:::warning
@tanstack/svelte-query v6 leverages Svelte 5's runes reactivity system. ZenStack is not compatible with prior versions that use Svelte stores.
:::
Not supported yet (need to migrate implementation from ZenStack v2).
Not supported yet.
<PackageInstall dependencies={['@zenstackhq/tanstack-query']} />
You can configure the query hooks by setting up context. The following options are available on the context:
-
endpoint
The endpoint to use for the queries. Defaults to "/api/model".
-
fetch
A custom
fetchfunction to use for the queries. Defaults to using built-infetch. -
logging
Logging configuration. Pass
trueto log withconsole.log. Pass a(message) => voidfunction for custom logging.
Example for using the context provider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QuerySettingsProvider, type FetchFn } from '@zenstackhq/tanstack-query/react';
// 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}>
<QuerySettingsProvider value={{ endpoint: '/api/model', fetch: myFetch }}>
<AppContent />
</QuerySettingsProvider>
</QueryClientProvider>
);
}
export default MyApp;<script setup lang="ts">
import { provideQuerySettingsContext, type FetchFn } from '@zenstackhq/tanstack-query/vue';
const myFetch: FetchFn = (url, options) => {
options = options ?? {};
options.headers = {
...options.headers,
'x-my-custom-header': 'hello world',
};
return fetch(url, options);
};
provideQuerySettingsContext({
endpoint: 'http://localhost:3000/api/model',
fetch: myFetch
});
</script>
<template>
<!-- App Content -->
</template><script lang="ts">
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
import { setQuerySettingsContext, type FetchFn } from '@zenstackhq/tanstack-query/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);
};
setQuerySettingsContext({
endpoint: '/api/model',
fetch: myFetch,
});
const queryClient = new QueryClient();
</script>
<div>
<QueryClientProvider client={queryClient}>
<slot />
</QueryClientProvider>
</div>The provided configuration can be overridden at query time. See Configuration Overrides for details.
Call the useClientQueries hook to get a root object to access CRUD hooks for all models. From there, using the hooks is pretty much the same as using ZenStackClient in backend code.
import { useClientQueries } from '@zenstackhq/tanstack-query/react';
import { schema } from '~/zenstack/schema-lite.ts';
const client = useClientQueries(schema);
// `usersWithPosts` is typed `User & { posts: Post[] }`
const { data: usersWithPosts } = client.user.useFindMany({
include: { posts: true },
orderBy: { createdAt: 'desc' },
});
const createPost = client.post.useCreate();
function onCreate() {
createPost.mutate({ title: 'Some new post' });
}:::info If you want the queries to be reactive, make sure to pass reactive objects as arguments when calling the hooks. See TanStack Query documentation for details. :::
<script setup lang="ts">
import { useClientQueries } from '@zenstackhq/tanstack-query/vue';
import { schema } from '~/zenstack/schema-lite.ts';
const client = useClientQueries(schema);
// `usersWithPosts` is typed `Ref<User & { posts: Post[] }>`
const { data: usersWithPosts } = client.user.useFindMany({
include: { posts: true },
orderBy: { createdAt: 'desc' },
});
const createPost = client.post.useCreate();
function onCreate() {
createPost.mutate({ title: 'Some new post' });
}
</script>:::info
Arguments to the query hooks must be wrapped in a function to make the result reactive. Please check this TanStack Query documentation if you are migrating from @tanstack/svelte-query v5 to v6.
:::
<script lang="ts">
import { useClientQueries } from '@zenstackhq/tanstack-query/svelte';
import { schema } from '~/zenstack/schema-lite.ts';
const client = useClientQueries(schema);
// `usersWithPosts` is typed `Ref<User & { posts: Post[] }>`
const { data: usersWithPosts } = client.user.useFindMany(
() => ({
include: { posts: true },
orderBy: { createdAt: 'desc' }})
);
</script>The useClientQueries takes the schema as an argument, and it uses it for both type inference and runtime logic (e.g., automatic query invalidation). This may bring security concerns, because the schema object contains sensitive content like access policies. Using it in the frontend code will expose such information.
To mitigate the risk, you can pass the additional --lite option when running zen generate. With that flag on, the CLI will generate an additional "lite" schema object in schema-lite.ts with all attributes removed. The lite schema contains all information needed by the query hooks. Check the CLI Reference for details.
The query configurations (as described in the Context Provider section) can be overridden in a hierarchical manner.
When calling useClientQueries, you can pass the endpoint, fetch, etc. options to override the global configuration for all hooks returned from the call.
const client = useClientQueries(schema, { endpoint: '/custom-endpoint' });Similarly, when calling an individual query or mutation hook, you can also pass the same options to override the configuration for that specific call.
const { data } = client.user.useFindMany(
{ where: { active: true } },
{ endpoint: '/another-endpoint' }
);Optimistic update is a technique that allows you to update the data cache immediately when a mutation executes while waiting for the server response. It helps achieve a more responsive UI. TanStack Query provides the infrastructure for implementing it.
The ZenStack-generated mutation hooks allow you to opt-in to "automatic optimistic update" by passing the optimisticUpdate option when calling the hook. When the mutation executes, it analyzes the current queries in the cache and tries to find the ones that need to be updated. When the mutation settles (either succeeded or failed), the queries are invalidated to trigger a re-fetch.
Here's an example:
const { mutate: create } = useCreatePost({ optimisticUpdate: true });
function onCreatePost() {
create({ ... })
}When mutate executes, if there are active queries like client.post.useFindMany(), the data of the mutation call will be optimistically inserted into the head of the query result.
By default, all queries opt into automatic optimistic update. You can opt-out on a per-query basis by passing false to the optimisticUpdate option.
const { data } = client.post.useFindMany(
{ where: { published: true } },
{ optimisticUpdate: false }
);When a query opts out, it won't be updated by a mutation, even if the mutation is set to update optimistically.
The useFindMany hook has an "infinite" variant that helps you build pagination or infinitely scrolling UIs.
Here's a quick example of using infinite query to load a list of posts with infinite pagination. See TanStack Query documentation for more details.
import { useClientQueries } from '@zenstackhq/tanstack-query/react';
// post list component with infinite loading
const Posts = () => {
const client = useClientQueries(schema);
const PAGE_SIZE = 10;
const fetchArgs = {
include: { author: true },
orderBy: { createdAt: 'desc' as const },
take: PAGE_SIZE,
};
const { data, fetchNextPage, hasNextPage } = client.post.useInfiniteFindMany(
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>
)}
</>
);
};Here's a quick example of using infinite query to load a list of posts with infinite pagination. See TanStack Query documentation for more details.
<script setup lang="ts">
// post list component with infinite loading
import { useClientQueries } from '@zenstackhq/tanstack-query/vue';
const client = useClientQueries(schema);
const PAGE_SIZE = 10;
const fetchArgs = {
include: { author: true },
orderBy: { createdAt: 'desc' as const },
take: PAGE_SIZE,
};
const { data, hasNextPage, fetchNextPage } = client.post.useInfiniteFindMany(
fetchArgs,
{
getNextPageParam: (lastPage, pages) => {
if (lastPage.length < PAGE_SIZE) {
return undefined;
}
const fetched = pages.flatMap((item) => item).length;
return {
...fetchArgs,
skip: fetched,
};
},
}
);
</script>
<template>
<div>
<ul v-if="data">
<template v-for="(posts, i) in data.pages" :key="i">
<li v-for="post in posts" :key="post.id">
{{ post.title }} by {{ post.author.email }}
</li>
</template>
</ul>
</div>
<button v-if="hasNextPage" @click="() => fetchNextPage()">Load More</button>
</template>Here's a quick example of using infinite query to load a list of posts with infinite pagination. See TanStack Query documentation for more details.
<script lang="ts">
// post list component with infinite loading
import { useClientQueries } from '@zenstackhq/tanstack-query/svelte';
const client = useClientQueries(schema);
const PAGE_SIZE = 10;
const fetchArgs = {
include: { author: true },
orderBy: { createdAt: 'desc' as const },
take: PAGE_SIZE,
};
const query = client.post.useInfiniteFindMany(
() => 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>Custom procedures are grouped under the $procs property on the client returned by useClientQueries. Query procedures are mapped to query hooks, while mutation procedures are mapped to mutation hooks.
There's no automatic query invalidation or optimistic update support for custom procedures, since their semantics are unknown to the system. You need to implement such behavior manually as needed.
:::info
The automatic invalidation is enabled by default, and you can use the invalidateQueries option to opt-out and handle revalidation by yourself.
useCreatePost({ invalidateQueries: false });:::
Query keys serve as unique identifiers for organizing the query cache. The generated hooks use the following query key scheme:
['zenstack', model, operation, args, flags]For example, the query key for
useFindUniqueUser({ where: { id: '1' } })will be:
['zenstack', 'User', 'findUnique', { where: { id: '1' } }, { infinite: false }]You can use the generated getQueryKey function to compute it.
The query hooks also return the query key as part of the result object.
const { data, queryKey } = useFindUniqueUser({ where: { id: '1' } });You can use TanStack Query's queryClient.cancelQueries API to cancel a query. The easiest way to do this is to use the queryKey returned by the query hook.
const queryClient = useQueryClient();
const { queryKey } = useFindUniqueUser({ where: { id: '1' } });
function onCancel() {
queryClient.cancelQueries({ queryKey, exact: true });
}When a cancellation occurs, the query state is reset and the ongoing fetch call to the CRUD API is aborted.
The following live demo shows how to use the query hooks in a React SPA.
https://github.com/zenstackhq/zenstack-v3/tree/main/samples/next.js
https://github.com/zenstackhq/zenstack-v3/tree/main/samples/nuxt
https://github.com/zenstackhq/zenstack-v3/tree/main/samples/sveltekit