diff --git a/.gitmodules b/.gitmodules index 217cf705..6baa5ba7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "code-repos/zenstackhq/v3-doc-server-adapter"] path = code-repos/zenstackhq/v3-doc-server-adapter url = https://github.com/zenstackhq/v3-doc-server-adapter +[submodule "code-repos/zenstackhq/v3-doc-tanstack-query"] + path = code-repos/zenstackhq/v3-doc-tanstack-query + url = https://github.com/zenstackhq/v3-doc-tanstack-query diff --git a/code-repos/zenstackhq/v3-doc-tanstack-query b/code-repos/zenstackhq/v3-doc-tanstack-query new file mode 160000 index 00000000..00ced0d2 --- /dev/null +++ b/code-repos/zenstackhq/v3-doc-tanstack-query @@ -0,0 +1 @@ +Subproject commit 00ced0d2badfee3bb5e0bac819a0ae062dce8e68 diff --git a/docs/reference/plugins/swr.mdx b/docs/reference/plugins/swr.mdx index bdcab130..ade67490 100644 --- a/docs/reference/plugins/swr.mdx +++ b/docs/reference/plugins/swr.mdx @@ -5,7 +5,7 @@ sidebar_position: 5 import Invalidation from './_invalidation.md'; import OptimisticBehavior from './_optimistic-behavior.md'; -import OptmisticLimitation from './_optimistic-limitation.md'; +import OptimisticLimitation from './_optimistic-limitation.md'; import FineGrainedOptimistic from './_fine-grained-optimistic.md'; # @zenstackhq/swr @@ -201,7 +201,7 @@ When `create` executes, if there are active queries like `useFindManyPost()`, th #### Limitations - + #### Opt-out diff --git a/docs/reference/plugins/tanstack-query.mdx b/docs/reference/plugins/tanstack-query.mdx index 3cb2999e..02e7a499 100644 --- a/docs/reference/plugins/tanstack-query.mdx +++ b/docs/reference/plugins/tanstack-query.mdx @@ -7,7 +7,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import Invalidation from './_invalidation.md'; import OptimisticBehavior from './_optimistic-behavior.md'; -import OptmisticLimitation from './_optimistic-limitation.md'; +import OptimisticLimitation from './_optimistic-limitation.md'; import FineGrainedOptimistic from './_fine-grained-optimistic.md'; # @zenstackhq/tanstack-query @@ -662,7 +662,7 @@ When `mutate` executes, if there are active queries like `useFindManyPost()`, th #### Limitations - + #### Opt-out diff --git a/docusaurus.config.js b/docusaurus.config.js index c68999ad..e7a66588 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -234,7 +234,7 @@ const config = { prism: { theme: prismThemes.github, darkTheme: prismThemes.dracula, - additionalLanguages: ['json'], + additionalLanguages: ['json', 'tsx'], }, zoom: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5631d504..4fd5ed04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1710,11 +1710,8 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001587: - resolution: {integrity: sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==} - - caniuse-lite@1.0.30001632: - resolution: {integrity: sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==} + caniuse-lite@1.0.30001753: + resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -7430,7 +7427,7 @@ snapshots: autoprefixer@10.4.13(postcss@8.4.21): dependencies: browserslist: 4.21.4 - caniuse-lite: 1.0.30001587 + caniuse-lite: 1.0.30001753 fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -7440,7 +7437,7 @@ snapshots: autoprefixer@10.4.17(postcss@8.4.35): dependencies: browserslist: 4.22.3 - caniuse-lite: 1.0.30001587 + caniuse-lite: 1.0.30001753 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.0 @@ -7450,7 +7447,7 @@ snapshots: autoprefixer@10.4.19(postcss@8.4.45): dependencies: browserslist: 4.23.1 - caniuse-lite: 1.0.30001632 + caniuse-lite: 1.0.30001753 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.0 @@ -7569,21 +7566,21 @@ snapshots: browserslist@4.21.4: dependencies: - caniuse-lite: 1.0.30001587 + caniuse-lite: 1.0.30001753 electron-to-chromium: 1.4.284 node-releases: 2.0.8 update-browserslist-db: 1.0.10(browserslist@4.21.4) browserslist@4.22.3: dependencies: - caniuse-lite: 1.0.30001587 + caniuse-lite: 1.0.30001753 electron-to-chromium: 1.4.667 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.3) browserslist@4.23.1: dependencies: - caniuse-lite: 1.0.30001632 + caniuse-lite: 1.0.30001753 electron-to-chromium: 1.4.796 node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.1) @@ -7627,13 +7624,11 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.23.1 - caniuse-lite: 1.0.30001587 + caniuse-lite: 1.0.30001753 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001587: {} - - caniuse-lite@1.0.30001632: {} + caniuse-lite@1.0.30001753: {} ccount@2.0.1: {} diff --git a/src/components/GithubCodeBlock.tsx b/src/components/GithubCodeBlock.tsx index 4f3692b7..311e6b68 100644 --- a/src/components/GithubCodeBlock.tsx +++ b/src/components/GithubCodeBlock.tsx @@ -11,6 +11,8 @@ const GithubCodeBlock: React.FC = ({ repoPath, file }) => const getLanguage = (file: string): string => { if (file.endsWith('.ts')) { return 'typescript'; + } else if (file.endsWith('.tsx')) { + return 'tsx'; } else if (file.endsWith('.zmodel')) { return 'zmodel'; } else { diff --git a/tsconfig.json b/tsconfig.json index 6f475698..41ffdec4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { - // This file is not used in compilation. It is here just for a nice editor experience. - "extends": "@tsconfig/docusaurus/tsconfig.json", - "compilerOptions": { - "baseUrl": "." - } + // This file is not used in compilation. It is here just for a nice editor experience. + "extends": "@tsconfig/docusaurus/tsconfig.json", + "compilerOptions": { + "baseUrl": "." + } } diff --git a/versioned_docs/version-1.x/reference/plugins/swr.mdx b/versioned_docs/version-1.x/reference/plugins/swr.mdx index 9aa6df66..1345c774 100644 --- a/versioned_docs/version-1.x/reference/plugins/swr.mdx +++ b/versioned_docs/version-1.x/reference/plugins/swr.mdx @@ -5,7 +5,7 @@ sidebar_position: 5 import Invalidation from './_invalidation.md'; import OptimisticBehavior from './_optimistic-behavior.md'; -import OptmisticLimitation from './_optimistic-limitation.md'; +import OptimisticLimitation from './_optimistic-limitation.md'; # @zenstackhq/swr @@ -170,7 +170,7 @@ When `create` executes, if there are active queries like `useFindManyPost()`, th #### Limitations - + #### Opt-out diff --git a/versioned_docs/version-1.x/reference/plugins/tanstack-query.mdx b/versioned_docs/version-1.x/reference/plugins/tanstack-query.mdx index 7292d370..0e2502a2 100644 --- a/versioned_docs/version-1.x/reference/plugins/tanstack-query.mdx +++ b/versioned_docs/version-1.x/reference/plugins/tanstack-query.mdx @@ -7,7 +7,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import Invalidation from './_invalidation.md'; import OptimisticBehavior from './_optimistic-behavior.md'; -import OptmisticLimitation from './_optimistic-limitation.md'; +import OptimisticLimitation from './_optimistic-limitation.md'; # @zenstackhq/tanstack-query @@ -430,7 +430,7 @@ When `mutate` executes, if there are active queries like `useFindManyPost()`, th #### Limitations - + #### Opt-out diff --git a/versioned_docs/version-3.x/reference/cli.md b/versioned_docs/version-3.x/reference/cli.md index ee3bde1b..bab698f6 100644 --- a/versioned_docs/version-3.x/reference/cli.md +++ b/versioned_docs/version-3.x/reference/cli.md @@ -44,6 +44,8 @@ Options: --schema schema file (with extension .zmodel). Defaults to "zenstack/schema.zmodel" unless specified in package.json. -o, --output default output directory for code generation + --lite also generate a lite version of schema without attributes + --lite-only only generate lite version of schema without attributes --silent suppress all output except errors (default: false) -h, --help display help for command ``` diff --git a/versioned_docs/version-3.x/reference/package.md b/versioned_docs/version-3.x/reference/package.md deleted file mode 100644 index a9cc4ad7..00000000 --- a/versioned_docs/version-3.x/reference/package.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Packages diff --git a/versioned_docs/version-3.x/service/client-sdk.md b/versioned_docs/version-3.x/service/client-sdk.md deleted file mode 100644 index e625ac3d..00000000 --- a/versioned_docs/version-3.x/service/client-sdk.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Client SDK 🚧 - -Coming soon. diff --git a/versioned_docs/version-3.x/service/client-sdk/_category_.yml b/versioned_docs/version-3.x/service/client-sdk/_category_.yml new file mode 100644 index 00000000..7e10488c --- /dev/null +++ b/versioned_docs/version-3.x/service/client-sdk/_category_.yml @@ -0,0 +1,4 @@ +position: 3 +label: Client SDK +collapsible: true +collapsed: true diff --git a/versioned_docs/version-3.x/service/client-sdk/index.md b/versioned_docs/version-3.x/service/client-sdk/index.md new file mode 100644 index 00000000..e64db281 --- /dev/null +++ b/versioned_docs/version-3.x/service/client-sdk/index.md @@ -0,0 +1,5 @@ +# Client SDK + +You can consume the query services by writing HTTP calls directly, but it's time consuming and error prone. Just as with deriving the CRUD service from the data model, we can derive client SDKs automatically, too. + +Today, ZenStack provides an [TanStack Query](./tanstack-query) integration that gives you a fully typed and feature-rich query client. Other types of client will be added in the future. diff --git a/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_category_.yml b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_category_.yml new file mode 100644 index 00000000..4b8c8b41 --- /dev/null +++ b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_category_.yml @@ -0,0 +1,3 @@ +position: 1 +collapsible: true +collapsed: true diff --git a/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_fine-grained-optimistic.md b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_fine-grained-optimistic.md new file mode 100644 index 00000000..52855152 --- /dev/null +++ b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_fine-grained-optimistic.md @@ -0,0 +1,25 @@ +Automatic optimistic update is convenient, but there might be cases where you want to explicitly control how the update happens. You can use the `optimisticUpdateProvider` callback to fully customize how each query cache entry should be optimistically updated. When the callback is set, it takes precedence over the automatic optimistic logic. + +```ts +client.post.useCreate({ + optimisticUpdateProvider: ({ queryModel, queryOperation, queryArgs, currentData, mutationArgs }) => { + return { kind: 'Update', data: ... /* computed result */ }; + } +}); +``` + +The callback is invoked for each query cache entry and receives the following arguments in a property bag: + +- `queryModel`: The model of the query, e.g., `Post`. +- `queryOperation`: The operation of the query, e.g., `findMany`, `count`. +- `queryArgs`: The arguments of the query, e.g., `{ where: { published: true } }`. +- `currentData`: The current cache data. +- `mutationArgs`: The arguments of the mutation, e.g., `{ data: { title: 'My awesome post' } }`. + +It should return a result object with the following fields: + +- `kind`: The kind of the optimistic update. + - `Update`: update the cache using the computed data + - `Skip`: skip the update + - `ProceedDefault`: use the default automatic optimistic behavior. +- `data`: The computed data to update the cache with. Only used when `kind` is `Update`. diff --git a/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_invalidation.md b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_invalidation.md new file mode 100644 index 00000000..a919ce28 --- /dev/null +++ b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_invalidation.md @@ -0,0 +1,42 @@ +The mutation hooks generated by ZenStack automatically invalidates the queries that are potentially affected by the changes. For example, if you create a `Post`, the `client.post.useFindMany()` query will be automatically invalidated when the mutation succeeds. Invalidation causes cache to be purged and fresh data to be refetched. + +The automatic invalidation takes care of nested read, write, and delete cascading. + +**1. Nested Read** + +Nested reads are also subject to automatic invalidation. For example, if you create a `Post`, the query made by + +```ts +client.user.useFindUnique({ where: { id: userId }, include: { posts: true } }); +``` + +will be invalidated. + +**2. Nested Write** + +Similarly, nested writes also trigger automatic invalidation. For example, if you create a `Post` in a nested update to `User` like: + +```ts +updateUser.mutate({ where: { id: userId }, posts: { create: { title: 'post1' } } }); +``` + +The mutation will cause queries like ```client.post.useFindMany()``` to be invalidated. + +**3. Delete Cascade** + +In ZModel, relations can be configured to cascade delete, e.g.: + +```zmodel +model User { + ... + posts Post[] +} + +model Post { + ... + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int +} +``` + +When a `User` is deleted, the `Post` entities it owns will be deleted automatically. The generated hooks takes cascade delete into account. For example, if you delete a `User`, `Post` model will be considered as affected and queries like ```client.post.useFindMany()``` will be invalidated. diff --git a/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_optimistic-behavior.md b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_optimistic-behavior.md new file mode 100644 index 00000000..6c54ff85 --- /dev/null +++ b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_optimistic-behavior.md @@ -0,0 +1,4 @@ +- `create` mutation inserts item to the head of the query results of the corresponding `useFindMany` queries. +- `update` mutation updates the item in the query results of `useFindXXX` queries and their nested reads by matching the item's ID. +- `upsert` mutation first tries to update the item in the query results of `useFindXXX` queries and their nested reads by matching the item's ID. If the item is not found, it inserts the item to the head of the query results. +- `delete` mutation removes the item from the query results of the corresponding `useFindMany` queries and sets `null` to `useFindUnique` and `useFindFirst` query results, by matching the item's ID. diff --git a/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_optimistic-limitation.md b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_optimistic-limitation.md new file mode 100644 index 00000000..1119d16c --- /dev/null +++ b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/_optimistic-limitation.md @@ -0,0 +1,4 @@ +- The automatic optimistic update relies on ID matching. It only works for queries that select the ID field(s). +- Non-entity-fetching queries like `count`, `aggregate`, and `groupBy` are not affected. +- Infinite queries are not affected. +- It doesn't respect filter conditions or access policies that potentially affect the queries under update. For example, for query `client.post.useFindMany({ where: { published: true }})`, when a non-published `Post` is created, it'll still be inserted into the query result. \ No newline at end of file diff --git a/versioned_docs/version-3.x/service/client-sdk/tanstack-query/index.md b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/index.md new file mode 100644 index 00000000..3090a6c1 --- /dev/null +++ b/versioned_docs/version-3.x/service/client-sdk/tanstack-query/index.md @@ -0,0 +1,471 @@ +--- +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'; + +# TanStack Query + +## Overview + +[TanStack Query](https://tanstack.com/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-handler/rpc.md) 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). + +## Installation + +:::info +TanStack Query version 5.0.0 or later is required. +::: + + + +## Context Provider + +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 `fetch` function to use for the queries. Defaults to using built-in `fetch`. + +- logging + + Enable logging for debugging purposes. Defaults to false. + +Example for using the context provider: + + + + + +```tsx title='_app.tsx' +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 ( + + + + + + ); +} + +export default MyApp; +``` + + + + + +```html title='App.vue' + + + +``` + + + + +```svelte title='+layout.svelte' + + +
+ + + +
+``` +
+ +
+ +## Using the Query Hooks + +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. + +```ts +// replace "/react" with "/vue", "/svelte" as needed +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' }); +} +``` + +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. + +## Optimistic Update + +### Automatic Optimistic Update + +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](https://tanstack.com/query/v5/docs/react/guides/optimistic-updates). + +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: + +```ts +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. + +#### Details of the optimistic behavior + + + +#### Limitations + + + +### Fine-Grained Optimistic Update + + + +### Opt-out + +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. + +```ts +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. + +## Infinite Query + +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](https://tanstack.com/query/v5/docs/react/guides/infinite-queries) for more details. + +```tsx title='/src/components/Posts.tsx' +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 ( + <> +
    + {data?.pages.map((posts, i) => ( + + {posts?.map((post) => ( +
  • + {post.title} by {post.author.email} +
  • + ))} +
    + ))} +
+ {hasNextPage && ( + + )} + + ); +}; +``` + +
+ + + +Here's a quick example of using infinite query to load a list of posts with infinite pagination. See [TanStack Query documentation](https://tanstack.com/query/v5/docs/vue/guides/infinite-queries) for more details. + +```html title='/src/components/Posts.vue' + + + + +``` + + + + + +Here's a quick example of using infinite query to load a list of posts with infinite pagination. See [TanStack Query documentation](https://tanstack.com/query/v5/docs/framework/svelte/examples/load-more-infinite-scroll) for more details. + +```svelte title='/src/components/Posts.svelte' + + +
+
    +
    + {#if $query.data} + {#each $query.data.pages as posts, i (i)} + {#each posts as post (post.id)} +
  • {post.title} by {post.author.email}
  • + {/each} + {/each} + {/if} +
    +
+ {#if $query.hasNextPage} + + {/if} +
+``` + +
+ +
+ +## Advanced Topics + +### Query Invalidation + + + +:::info + +The automatic invalidation is enabled by default, and you can use the `invalidateQueries` option to opt-out and handle revalidation by yourself. + +```ts +useCreatePost({ invalidateQueries: false }); +``` +::: + +### Query Key + +Query keys serve as unique identifiers for organizing the query cache. The generated hooks use the following query key scheme: + +```ts +['zenstack', model, operation, args, flags] +``` + +For example, the query key for + +```ts +useFindUniqueUser({ where: { id: '1' } }) +``` + +will be: + +```ts +['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. + +```ts +const { data, queryKey } = useFindUniqueUser({ where: { id: '1' } }); +``` + +### Query Cancellation + +You can use TanStack Query's [`queryClient.cancelQueries`](https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientcancelqueries) API to cancel a query. The easiest way to do this is to use the `queryKey` returned by the query hook. + +```ts +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. + +## Example + +The following live demo shows how to use the query hooks in a React SPA. + + +