Repository: liaoliao666/react-query-kit Branch: main Commit: a00020f8ca8c Files: 35 Total size: 94.4 KB Directory structure: gitextract_dtwmuxyq/ ├── .browserslistrc ├── .commitlintrc.json ├── .eslintrc ├── .github/ │ └── workflows/ │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .husky/ │ └── commit-msg ├── .npmrc ├── .nvmrc ├── LICENSE ├── README-zh_CN.md ├── README.md ├── babel.config.js ├── jest.config.js ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src/ │ ├── createBaseQuery.ts │ ├── createInfiniteQuery.ts │ ├── createMutation.ts │ ├── createQuery.ts │ ├── createSuspenseInfiniteQuery.ts │ ├── createSuspenseQuery.ts │ ├── index.ts │ ├── router.ts │ ├── types.ts │ └── utils.ts ├── tests/ │ ├── createInfiniteQuery.test.tsx │ ├── createMutation.test.tsx │ ├── createQuery.test.tsx │ ├── router.test.tsx │ ├── types.typecheck.ts │ └── utils.tsx ├── tsconfig.json └── tsconfig.types.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .browserslistrc ================================================ # Browsers we support Chrome >= 73 Firefox >= 78 Edge >= 79 Safari >= 12.0 iOS >= 12.0 opera >= 53 ================================================ FILE: .commitlintrc.json ================================================ { "extends": ["@commitlint/config-conventional"] } ================================================ FILE: .eslintrc ================================================ { "env": { "browser": true, "shared-node-browser": true, "node": true, "es6": true }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "plugin:prettier/recommended", "plugin:react-hooks/recommended", "plugin:import/errors", "plugin:import/warnings" ], "plugins": [ "@typescript-eslint", "react", "prettier", "react-hooks", "import", "jest" ], "parser": "@typescript-eslint/parser", "parserOptions": { "project": 2018, "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "rules": { "eqeqeq": "error", "no-var": "error", "prefer-const": "error", "curly": ["warn", "multi-line", "consistent"], "no-console": "off", "@typescript-eslint/no-non-null-assertion": "off", "import/no-unresolved": ["error", { "commonjs": true, "amd": true }], "import/export": "error", "@typescript-eslint/ban-types": "off", "@typescript-eslint/no-duplicate-imports": ["error"], "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-unused-vars": [ "warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } ], "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/ban-ts-comment": "off", "jest/consistent-test-it": [ "error", { "fn": "it", "withinDescribe": "it" } ], "import/order": "off", "react/jsx-uses-react": "off", "react/react-in-jsx-scope": "off", "sort-imports": [ "error", { "ignoreDeclarationSort": true } ] }, "settings": { "react": { "version": "detect" }, "import/extensions": [".js", ".jsx", ".ts", ".tsx"], "import/parsers": { "@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx"] }, "import/resolver": { "node": { "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], "paths": ["src"] } } }, "overrides": [ { "files": ["src"], "parserOptions": { "project": "./tsconfig.json" } }, { "files": ["tests/**/*.tsx"], "env": { "jest/globals": true } }, { "files": ["./*.js"], "rules": { "@typescript-eslint/no-var-requires": "off" } } ] } ================================================ FILE: .github/workflows/publish.yml ================================================ name: 'publish' on: push: branches: - main jobs: release: name: publish runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version-file: .nvmrc registry-url: https://registry.npmjs.org cache: 'yarn' - run: | git config --global user.name 'liaoliao666' git config --global user.email '1076988944@qq.com' yarn yarn test && yarn build - uses: JS-DevTools/npm-publish@v1 with: token: ${{ secrets.NPM_AUTH_TOKEN }} ================================================ FILE: .github/workflows/tests.yml ================================================ name: Tests on: push: branches: - main jobs: tests: name: Building package runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v3 - name: Caching node_modules uses: actions/cache@v3 id: yarn-cache with: path: "**/node_modules" key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-node- - name: Setup Node uses: actions/setup-node@v3 with: node-version-file: '.nvmrc' cache: 'yarn' - name: Install dependencies 🔧 if: steps.yarn-cache.outputs.cache-hit != 'true' run: yarn install --frozen-lockfile - name: Test package 🔧 run: yarn test ================================================ FILE: .gitignore ================================================ node_modules # builds build # misc npm-debug.log* yarn-debug.log* yarn-error.log* yarn.lock package-lock.json size-plugin.json stats.json stats.html # mac .DS_Store ================================================ FILE: .husky/commit-msg ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx commitlint --edit ================================================ FILE: .npmrc ================================================ auto-install-peers=true registry=https://registry.npmjs.org ================================================ FILE: .nvmrc ================================================ v16.19.0 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 liaoliao666 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README-zh_CN.md ================================================


🕊️ 一个用于 ReactQuery 的工具包,它能使 ReactQuery 更易复用和类型安全

Latest build Latest published version Types included License Number of downloads GitHub Stars

--- ## Motivation - 以类型安全的方式管理 `queryKey` - 让 `queryClient` 的操作更清楚地关联到哪个自定义 hook - 可以从任何自定义 ReactQuery hook 中提取的 TypeScript 类型 - 中间件 [English](./README.md) | 简体中文 ## Table of Contents - [安装](#installation) - [例子](#examples) - 使用 - [createQuery](#createquery) - [createInfiniteQuery](#createinfinitequery) - [createSuspenseQuery](#createsuspensequery) - [createSuspenseInfiniteQuery](#createsuspenseinfinitequery) - [createMutation](#createmutation) - [router](#router) - [中间件](#中间件) - [TypeScript](#typescript) - [类型推导](#类型推导) - [禁用查询](#禁用查询) - [常见问题](#常见问题) - [迁移](#迁移) - [Issues](#issues) - [🐛 Bugs](#-bugs) - [💡 Feature Requests](#-feature-requests) - [LICENSE](#license) ## Installation ```bash $ npm i react-query-kit # or $ yarn add react-query-kit ``` 如果您还在使用 React Query Kit v2? 请在此处查看 v2 文档:https://github.com/liaoliao666/react-query-kit/tree/v2#readme. # Examples - [Basic](https://codesandbox.io/s/example-react-query-kit-basic-1ny2j8) - [Optimistic Updates](https://codesandbox.io/s/example-react-query-kit-optimistic-updates-eefg0v) - [Next.js](https://codesandbox.io/s/example-react-query-kit-nextjs-uldl88) - [Load-More & Infinite Scroll](https://codesandbox.io/s/example-react-query-kit-load-more-infinite-scroll-vg494v) ## createQuery ### Usage ```tsx import { QueryClient, dehydrate } from '@tanstack/react-query' import { createQuery } from 'react-query-kit' type Data = { title: string; content: string } type Variables = { id: number } const usePost = createQuery({ queryKey: ['posts'], fetcher: (variables: Variables): Promise => { return fetch(`/posts/${variables.id}`).then(res => res.json()) }, // 你还可以通过中间件来定制这个 hook 的行为 use: [myMiddleware] }) const variables = { id: 1 } // example export default function Page() { // queryKey 相等于 ['/posts', { id: 1 }] const { data } = usePost({ variables }) return (
{data?.title}
{data?.content}
) } console.log(usePost.getKey()) // ['/posts'] console.log(usePost.getKey(variables)) // ['/posts', { id: 1 }] // nextjs 例子 export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery(usePost.getFetchOptions(variables)) return { props: { dehydratedState: dehydrate(queryClient), }, } } // 在 react 组件外使用 const data = await queryClient.fetchQuery( usePost.getFetchOptions(variables) ) // useQueries 例子 const queries = useQueries({ queries: [ usePost.getOptions(variables), useUser.getOptions(), ], }) // getQueryData queryClient.getQueryData(usePost.getKey(variables)) // Data // setQueryData queryClient.setQueryData(usePost.getKey(variables), {...}) ``` ### 额外的 API 文档 Options - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` - 必填 - 用于请求数据的函数。 第二个参数是“queryFn”的“QueryFunctionContext” - `variables?: TVariables` - 可选 - `variables` 将是 fetcher 的第一个参数和 `queryKey` 数组的最后一个元素 - `use: Middleware[]` - 可选 - 中间件函数数组 [(详情)](#中间件) Expose Methods - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` - `getKey: (variables: TVariables) => QueryKey` - `getOptions: (variables: TVariables) => UseInfiniteQueryOptions` - `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn })` ## createInfiniteQuery ### Usage ```tsx import { QueryClient, dehydrate } from '@tanstack/react-query' import { createInfiniteQuery } from 'react-query-kit' type Data = { projects: { id: string; name: string }[]; nextCursor: number } type Variables = { active: boolean } const useProjects = createInfiniteQuery({ queryKey: ['projects'], fetcher: (variables: Variables, { pageParam }): Promise => { return fetch( `/projects?cursor=${pageParam}?active=${variables.active}` ).then(res => res.json()) }, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, initialPageParam: 0, }) const variables = { active: true } // example export default function Page() { // queryKey equals to ['projects', { active: true }] const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = useProjects({ variables }) return (
{data.pages.map((group, i) => ( {group.projects.map(project => (

{project.name}

))}
))}
{isFetching && !isFetchingNextPage ? 'Fetching...' : null}
) } // nextjs example export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchInfiniteQuery( useProjects.getFetchOptions(variables) ) return { props: { dehydratedState: dehydrate(queryClient), }, } } // 在 react 组件外使用 const data = await queryClient.fetchInfiniteQuery( useProjects.getFetchOptions(variables) ) ``` ### 额外的 API 文档 Options - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` - 必填 - 查询将用于请求数据的函数。 第二个参数是“queryFn”的“QueryFunctionContext” - `variables?: TVariables` - 可选 - `variables` 将是 fetcher 的第一个参数和 `queryKey` 数组的最后一个元素 - `use: Middleware[]` - 可选 - 中间件函数数组 [(详情)](#中间件) Expose Methods - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` - `getKey: (variables: TVariables) => QueryKey` - `getOptions: (variables: TVariables) => UseInfiniteQueryOptions` - `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn, getNextPageParam, getPreviousPageParam, initialPageParam })` ## createSuspenseQuery 这与在查询配置中将 suspense 选项设置为 true 具有相同的效果,但在 TypeScript 的体验更好,因为 data 是有定义的(因为错误和加载状态由 Suspense 和 ErrorBoundaries 处理)。 ```ts import { createSuspenseQuery } from 'react-query-kit' createSuspenseQuery({ ...options, }) // 相当于 createQuery({ ...options, enabled: true, suspense: true, throwOnError: true, }) ``` ## createSuspenseInfiniteQuery ```ts import { createSuspenseInfiniteQuery } from 'react-query-kit' createSuspenseInfiniteQuery({ ...options, }) // 相当于 createInfiniteQuery({ ...options, enabled: true, suspense: true, throwOnError: true, }) ``` ## createMutation ### Usage ```tsx import { createMutation } from 'react-query-kit' const useAddTodo = createMutation( async (variables: { title: string; content: string }) => fetch('/post', { method: 'POST', body: JSON.stringify(variables), }).then(res => res.json()), { onSuccess(data, variables, context) { // do somethings }, } ) function App() { const mutation = useAddTodo({ onSettled: (data, error, variables, context) => { // Error or success... doesn't matter! }, }) return (
{mutation.isPending ? ( 'Adding todo...' ) : ( <> {mutation.isError ? (
An error occurred: {mutation.error.message}
) : null} {mutation.isSuccess ?
Todo added!
: null} )}
) } // usage outside of react component useAddTodo.mutationFn({ title: 'Do Laundry', content: 'content...' }) ``` ### 额外的 API 文档 Options - `use: Middleware[]` - 可选 - 中间件函数数组 [(详情)](#中间件) Expose Methods - `getKey: () => MutationKey` - `getOptions: () => UseMutationOptions` - `mutationFn: ExposeMutationFn` ## router `router` 允许您创建整个 API 的形状 ### Usage ```tsx import { router } from 'react-query-kit' const post = router(`post`, { byId: router.query({ fetcher: (variables: { id: number }) => fetch(`/posts/${variables.id}`).then(res => res.json()), use: [myMiddleware], }), list: router.infiniteQuery({ fetcher: (_variables, { pageParam }) => fetch(`/posts/?cursor=${pageParam}`).then(res => res.json()), getNextPageParam: lastPage => lastPage.nextCursor, initialPageParam: 0, }), add: router.mutation({ mutationFn: async (variables: { title: string; content: string }) => fetch('/posts', { method: 'POST', body: JSON.stringify(variables), }).then(res => res.json()), }), // nest router command: { report: router.mutation({ mutationFn }), promote: router.mutation({ mutationFn }), }, }) // get root key post.getKey() // ['post'] // hooks post.byId.useQuery({ variables: { id: 1 } }) post.byId.useSuspenseQuery({ variables: { id: 1 } }) post.list.useInfiniteQuery() post.list.useSuspenseInfiniteQuery() post.add.useMutation() post.command.report.useMutation() // expose methods post.byId.getKey({ id: 1 }) // ['post', 'byId', { id: 1 }] post.byId.getFetchOptions({ id: 1 }) post.byId.getOptions({ id: 1 }) post.byId.fetcher({ id: 1 }) post.add.getKey() // ['post', 'add'] post.add.getOptions() post.add.mutationFn({ title: 'title', content: 'content' }) // infer types type Data = inferData type FnData = inferFnData type Variables = inferVariables type Error = inferError ``` ### 合并路由 ```ts import { router } from 'react-query-kit' const user = router(`user`, {}) const post = router(`post`, {}) const k = { user, post, } ``` ### API 文档 `type Router = (key: string | unknown[], config: TConfig) => TRouter` Expose Methods - `query` 与 `createQuery` 类似,但无需选项 `queryKey` - `infiniteQuery` 与 `createInfiniteQuery` 类似,但无需选项 `queryKey` - `mutation` 与 `createMutation` 类似,但无需选项 `mutationKey` ## 中间件 此功能的灵感来自于 [SWR 的中间件功能](https://swr.vercel.app/docs/middleware)。 中间件接收 hook,可以在运行它之前和之后执行逻辑。如果有多个中间件,则每个中间件包装下一个中间件。列表中的最后一个中间件将接收原始的 hook。 ### 使用 ```ts import { QueryClient } from '@tanstack/react-query' import { Middleware, MutationHook, QueryHook, getKey } from 'react-query-kit' const logger: Middleware> = useQueryNext => { return options => { const log = useLogger() const fetcher = (variables, context) => { log(context.queryKey, variables) return options.fetcher(variables, context) } return useQueryNext({ ...options, fetcher, }) } } const useUser = createQuery({ use: [logger], }) // 全局中间件 const queryMiddleware: Middleware = useQueryNext => { return options => { // 你还可以通过函数 getKey 获取 queryKey const fullKey = getKey(options.queryKey, options.variables) // ... return useQueryNext(options) } } const mutationMiddleware: Middleware = useMutationNext => { return options => { // ... return useMutationNext(options) } } const queryClient = new QueryClient({ defaultOptions: { queries: { use: [queryMiddleware], }, mutations: { use: [mutationMiddleware], }, }, }) ``` ### 扩展 中间件将从上级合并。例如: ```jsx const queryClient = new QueryClient({ defaultOptions: { queries: { use: [a], }, }, }) const useSomething = createQuery({ use: [b], }) useSomething({ use: [c] }) ``` 相当于: ```js createQuery({ use: [a, b, c] }) ``` ### 多个中间件 每个中间件包装下一个中间件,最后一个只包装 useQuery hook。例如: ```jsx createQuery({ use: [a, b, c] }) ``` 中间件执行的顺序是 `a → b → c`,如下所示: ```plaintext enter a enter b enter c useQuery() exit c exit b exit a ``` ### 多个 QueryClient 在 ReactQuery v5 中,`QueryClient` 将是 `useQuery` 和 `useMutation` 的第二个参数。 如果你在全局中有多个 `QueryClient`,你应该在中间件钩子中接收 `QueryClient` ```ts const useSomething = createQuery({ use: [ function myMiddleware(useQueryNext) { // 你应该接收 queryClient 作为第二个参数 return (options, queryClient) => { const client = useQueryClient(queryClient) // ... return useQueryNext(options, queryClient) } }, ], }) // 如果你传入另一个 QueryClient useSomething({...}, anotherQueryClient) ``` ## TypeScript 默认情况下,ReactQueryKit 还会从 `fetcher` 推断 `data` 和 `variables` 的类型,因此您可以自动获得首选类型。 ```ts type Data = { title: string; content: string } type Variables = { id: number } const usePost = createQuery({ queryKey: ['posts'], fetcher: (variables: Variables): Promise => { return fetch(`/posts/${variables}`).then(res => res.json()) }, }) // `data` 将被推断为 `Data | undefined`. // `variables` 将被推断为 `Variables`. const { data } = usePost({ variables: { id: 1 } }) ``` 您还可以显式指定 `fetcher` 参数和返回的类型。 ```ts type Data = { title: string; content: string } type Variables = { id: number } const usePost = createQuery({ queryKey: ['posts'], fetcher: variables => { return fetch(`/posts/${variables}`).then(res => res.json()) }, }) // `data` 将被推断为 `Data | undefined`. // `error` 将被推断为 `Error | null` // `variables` 将被推断为 `Variables`. const { data, error } = usePost({ variables: { id: 1 } }) ``` ## 类型推导 您可以使用 `inferData` 或 `inferVariables` 提取任何自定义 hook 的 TypeScript 类型 ```ts import { inferData, inferFnData, inferError, inferVariables, inferOptions } from 'react-query-kit' const useProjects = createInfiniteQuery(...) inferData // InfiniteData inferFnData // Data inferVariables // Variables inferError // Error inferOptions // InfiniteQueryHookOptions<...> ``` ## 禁用查询 要禁用查询,您可以将 `skipToken` 作为选项 `variables` 传递给您的自定义查询。这将阻止查询被执行。 ```ts import { skipToken } from '@tanstack/react-query' const [name, setName] = useState() const result = usePost({ variables: id ? { id: id } : skipToken, }) // 以及用于 useQueries 的示例 const queries = useQueries({ queries: [usePost.getOptions(id ? { id: id } : skipToken)], }) ``` ## 常见问题 ### `getFetchOptions` 和 `getOptions` 有什么不同 `getFetchOptions` 只会返回必要的选项,而像 `staleTime` 和 `retry` 等选项会被忽略 ### `fetcher` 和 `queryFn` 有什么不同 ReactQueryKit 会自动将 `fetcher` 转换为 `queryFn`,例如 ```ts const useTest = createQuery({ queryKey: ['test'], fetcher: (variables, context) => { // ... }, }) // => useTest.getOptions(variables): // { // queryKey: ['test', variables], // queryFn: (context) => fetcher(variables, context) // } ``` ## 迁移 从 ReactQueryKit 2 升级 → ReactQueryKit 3 ```diff createQuery({ - primaryKey: 'posts', - queryFn: ({ queryKey: [_primaryKey, variables] }) => {}, + queryKey: ['posts'], + fetcher: variables => {}, }) ``` 您可以从 ReactQueryKit 3 中受益 - 支持传入数组 `queryKey` - 支持推断 fetcher 的类型,您可以自动享受首选的类型。 - 支持创建整个 API 的形状 ## Issues _Looking to contribute? Look for the [Good First Issue][good-first-issue] label._ ### 🐛 Bugs 请针对错误、缺少文档或意外行为提出问题。 [**See Bugs**][bugs] ### 💡 Feature Requests 请提交问题以建议新功能。 通过添加对功能请求进行投票 一个 👍。 这有助于维护人员优先处理要处理的内容。 [**See Feature Requests**][requests] ## LICENSE MIT [npm]: https://www.npmjs.com [node]: https://nodejs.org [bugs]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug [requests]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement [good-first-issue]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22 ================================================ FILE: README.md ================================================


🕊️ A toolkit for ReactQuery that make ReactQuery reusable and typesafe

Latest build Latest published version Types included License Number of downloads GitHub Stars

--- ## What could you benefit from it - Manage `queryKey` in a type-safe way - Make `queryClient`'s operations clearly associated with custom ReactQuery hooks - You can extract the TypeScript type of any custom ReactQuery hooks - Middleware English | [简体中文](./README-zh_CN.md) ## Table of Contents - [Installation](#installation) - [Examples](#examples) - Usage - [createQuery](#createquery) - [createInfiniteQuery](#createinfinitequery) - [createSuspenseQuery](#createsuspensequery) - [createSuspenseInfiniteQuery](#createsuspenseinfinitequery) - [createMutation](#createmutation) - [router](#router) - [Middleware](#middleware) - [TypeScript](#typescript) - [Type inference](#type-inference) - [Disabling Queries](#disabling-queries) - [FAQ](#faq) - [Migration](#migration) - [Issues](#issues) - [🐛 Bugs](#-bugs) - [💡 Feature Requests](#-feature-requests) - [LICENSE](#license) ## Installation This module is distributed via [npm][npm] which is bundled with [node][node] and should be installed as one of your project's `dependencies`: ```bash $ npm i react-query-kit # or $ yarn add react-query-kit ``` If you still on React Query Kit v2? Check out the v2 docs here: https://github.com/liaoliao666/react-query-kit/tree/v2#readme. # Examples - [Basic](https://codesandbox.io/s/example-react-query-kit-basic-1ny2j8) - [Optimistic Updates](https://codesandbox.io/s/example-react-query-kit-optimistic-updates-eefg0v) - [Next.js](https://codesandbox.io/s/example-react-query-kit-nextjs-uldl88) - [Load-More & Infinite Scroll](https://codesandbox.io/s/example-react-query-kit-load-more-infinite-scroll-vg494v) ## createQuery ### Usage ```tsx import { QueryClient, dehydrate } from '@tanstack/react-query' import { createQuery } from 'react-query-kit' type Data = { title: string; content: string } type Variables = { id: number } const usePost = createQuery({ queryKey: ['posts'], fetcher: (variables: Variables): Promise => { return fetch(`/posts/${variables.id}`).then(res => res.json()) }, // u can also pass middleware to cutomize this hook's behavior use: [myMiddleware] }) const variables = { id: 1 } // example export default function Page() { // queryKey will be `['posts', { id: 1 }]` if u passed variables const { data } = usePost({ variables }) return (
{data?.title}
{data?.content}
) } console.log(usePost.getKey()) // ['posts'] console.log(usePost.getKey(variables)) // ['posts', { id: 1 }] // nextjs example export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery(usePost.getFetchOptions(variables)) return { props: { dehydratedState: dehydrate(queryClient), }, } } // usage outside of react component const data = await queryClient.fetchQuery(usePost.getFetchOptions(variables)) // useQueries example const queries = useQueries({ queries: [ usePost.getOptions(variables), useUser.getOptions(), ], }) // getQueryData queryClient.getQueryData(usePost.getKey(variables)) // Data // setQueryData queryClient.setQueryData(usePost.getKey(variables), {...}) ``` ### Additional API Reference Options - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` - Required - The function that the query will use to request data. And The second param is the `QueryFunctionContext` of `queryFn`. - `variables?: TVariables` - Optional - `variables` will be the frist param of fetcher and the last element of the `queryKey` array - `use: Middleware[]` - Optional - array of middleware functions [(details)](#middleware) Expose Methods - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` - `getKey: (variables: TVariables) => QueryKey` - `getOptions: (variables: TVariables) => UseQueryOptions` - `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn })` ## createInfiniteQuery ### Usage ```tsx import { QueryClient, dehydrate } from '@tanstack/react-query' import { createInfiniteQuery } from 'react-query-kit' type Data = { projects: { id: string; name: string }[]; nextCursor: number } type Variables = { active: boolean } const useProjects = createInfiniteQuery({ queryKey: ['projects'], fetcher: (variables: Variables, { pageParam }): Promise => { return fetch( `/projects?cursor=${pageParam}?active=${variables.active}` ).then(res => res.json()) }, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, initialPageParam: 0, }) const variables = { active: true } // example export default function Page() { // queryKey equals to ['projects', { active: true }] const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = useProjects({ variables }) return (
{data.pages.map((group, i) => ( {group.projects.map(project => (

{project.name}

))}
))}
{isFetching && !isFetchingNextPage ? 'Fetching...' : null}
) } // nextjs example export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchInfiniteQuery( useProjects.getFetchOptions(variables) ) return { props: { dehydratedState: dehydrate(queryClient), }, } } // usage outside of react component const data = await queryClient.fetchInfiniteQuery( useProjects.getFetchOptions(variables) ) ``` ### Additional API Reference Options - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` - Required - The function that the query will use to request data. And The second param is the `QueryFunctionContext` of `queryFn`. - `variables?: TVariables` - Optional - `variables` will be the frist param of fetcher and the last element of the `queryKey` array - `use: Middleware[]` - Optional - array of middleware functions [(details)](#middleware) Expose Methods - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` - `getKey: (variables: TVariables) => QueryKey` - `getOptions: (variables: TVariables) => UseInfiniteQueryOptions` - `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn, getNextPageParam, getPreviousPageParam, initialPageParam })` ## createSuspenseQuery This has the same effect as setting the `suspense` option to `true` in the query config, but it works better in TypeScript, because `data` is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries). ```ts import { createSuspenseQuery } from 'react-query-kit' createSuspenseQuery({ ...options, }) // equals to createQuery({ ...options, enabled: true, suspense: true, throwOnError: true, }) ``` ## createSuspenseInfiniteQuery ```ts import { createSuspenseInfiniteQuery } from 'react-query-kit' createSuspenseInfiniteQuery({ ...options, }) // equals to createInfiniteQuery({ ...options, enabled: true, suspense: true, throwOnError: true, }) ``` ## createMutation ### Usage ```tsx import { createMutation } from 'react-query-kit' const useAddTodo = createMutation({ mutationFn: async (variables: { title: string; content: string }) => fetch('/post', { method: 'POST', body: JSON.stringify(variables), }).then(res => res.json()), onSuccess(data, variables, context) { // do somethings }, }) function App() { const mutation = useAddTodo({ onSettled: (data, error, variables, context) => { // Error or success... doesn't matter! }, }) return (
{mutation.isPending ? ( 'Adding todo...' ) : ( <> {mutation.isError ? (
An error occurred: {mutation.error.message}
) : null} {mutation.isSuccess ?
Todo added!
: null} )}
) } // usage outside of react component useAddTodo.mutationFn({ title: 'Do Laundry', content: 'content...' }) ``` ### Additional API Reference Options - `use: Middleware[]` - Optional - array of middleware functions [(details)](#middleware) Expose Methods - `getKey: () => MutationKey` - `getOptions: () => UseMutationOptions` - `mutationFn: ExposeMutationFn` ## router `router` which allow you to create a shape of your entire API ### Usage ```tsx import { router } from 'react-query-kit' const post = router(`post`, { byId: router.query({ fetcher: (variables: { id: number }) => fetch(`/posts/${variables.id}`).then(res => res.json()), use: [myMiddleware], }), list: router.infiniteQuery({ fetcher: (_variables, { pageParam }) => fetch(`/posts/?cursor=${pageParam}`).then(res => res.json()), getNextPageParam: lastPage => lastPage.nextCursor, initialPageParam: 0, }), add: router.mutation({ mutationFn: async (variables: { title: string; content: string }) => fetch('/posts', { method: 'POST', body: JSON.stringify(variables), }).then(res => res.json()), }), // nest router command: { report: router.mutation({ mutationFn }), promote: router.mutation({ mutationFn }), }, }) // get root key post.getKey() // ['post'] // hooks post.byId.useQuery({ variables: { id: 1 } }) post.byId.useSuspenseQuery({ variables: { id: 1 } }) post.list.useInfiniteQuery() post.list.useSuspenseInfiniteQuery() post.add.useMutation() post.command.report.useMutation() // expose methods post.byId.getKey({ id: 1 }) // ['post', 'byId', { id: 1 }] post.byId.getFetchOptions({ id: 1 }) post.byId.getOptions({ id: 1 }) post.byId.fetcher({ id: 1 }) post.add.getKey() // ['post', 'add'] post.add.getOptions() post.add.mutationFn({ title: 'title', content: 'content' }) // infer types type Data = inferData type FnData = inferFnData type Variables = inferVariables type Error = inferError ``` ### Merging Routers ```ts import { router } from 'react-query-kit' const user = router(`user`, {}) const post = router(`post`, {}) const k = { user, post, } ``` ### API Reference `type Router = (key: string | unknown[], config: TConfig) => TRouter` Expose Methods - `query` Similar to `createQuery` but without option `queryKey` - `infiniteQuery` Similar to `createInfiniteQuery` but without option `queryKey` - `mutation` Similar to `createMutation` but without option `mutationKey` ## Middleware This feature is inspired by the [Middleware feature from SWR](https://swr.vercel.app/docs/middleware). The middleware feature is a new addition in ReactQueryKit 1.5.0 that enables you to execute logic before and after hooks. Middleware receive the hook and can execute logic before and after running it. If there are multiple middleware, each middleware wraps the next middleware. The last middleware in the list will receive the original hook. ### Usage ```ts import { QueryClient } from '@tanstack/react-query' import { Middleware, MutationHook, QueryHook, getKey } from 'react-query-kit' const logger: Middleware> = useQueryNext => { return options => { const log = useLogger() const fetcher = (variables, context) => { log(context.queryKey, variables) return options.fetcher(variables, context) } return useQueryNext({ ...options, fetcher, }) } } const useUser = createQuery({ use: [logger], }) // global middlewares const queryMiddleware: Middleware = useQueryNext => { return options => { // u can also get queryKey via function getKey const fullKey = getKey(options.queryKey, options.variables) // ... return useQueryNext(options) } } const mutationMiddleware: Middleware = useMutationNext => { return options => { // ... return useMutationNext(options) } } const queryClient = new QueryClient({ defaultOptions: { queries: { use: [queryMiddleware], }, mutations: { use: [mutationMiddleware], }, }, }) ``` ### Extend Middleware will be merged from superior. For example: ```jsx const queryClient = new QueryClient({ defaultOptions: { queries: { use: [a], }, }, }) const useSomething = createQuery({ use: [b], }) useSomething({ use: [c] }) ``` is equivalent to: ```js createQuery({ use: [a, b, c] }) ``` ### Multiple Middleware Each middleware wraps the next middleware, and the last one just wraps the useQuery. For example: ```jsx createQuery({ use: [a, b, c] }) ``` The order of middleware executions will be a → b → c, as shown below: ```plaintext enter a enter b enter c useQuery() exit c exit b exit a ``` ### Multiple QueryClient In ReactQuery v5, the `QueryClient` will be the second argument to `useQuery` and `useMutation`. If u have multiple `QueryClient` in global, u should receive `QueryClient` in middleware hook. ```ts const useSomething = createQuery({ use: [ function myMiddleware(useQueryNext) { // u should receive queryClient as the second argument here return (options, queryClient) => { const client = useQueryClient(queryClient) // ... return useQueryNext(options, queryClient) } }, ], }) // if u need to pass an another QueryClient useSomething({...}, anotherQueryClient) ``` ## TypeScript By default, ReactQueryKit will also infer the types of `data` and `variables` from `fetcher`, so you can have the preferred types automatically. ```ts type Data = { title: string; content: string } type Variables = { id: number } const usePost = createQuery({ queryKey: ['posts'], fetcher: (variables: Variables): Promise => { return fetch(`/posts/${variables}`).then(res => res.json()) }, }) // `data` will be inferred as `Data | undefined`. // `variables` will be inferred as `Variables`. const { data } = usePost({ variables: { id: 1 } }) ``` You can also explicitly specify the types for `fetcher`‘s `variables` and `data`. ```ts type Data = { title: string; content: string } type Variables = { id: number } const usePost = createQuery({ queryKey: ['posts'], fetcher: variables => { return fetch(`/posts/${variables}`).then(res => res.json()) }, }) // `data` will be inferred as `Data | undefined`. // `error` will be inferred as `Error | null` // `variables` will be inferred as `Variables`. const { data, error } = usePost({ variables: { id: 1 } }) ``` ## Type inference You can extract the TypeScript type of any custom hook with `inferData` or `inferVariables` ```ts import { inferData, inferFnData, inferError, inferVariables, inferOptions } from 'react-query-kit' const useProjects = createInfiniteQuery(...) inferData // InfiniteData inferFnData // Data inferVariables // Variables inferError // Error inferOptions // InfiniteQueryHookOptions<...> ``` ## Disabling Queries To disable queries, you can pass `skipToken` as the option `variables` to your custom query. This will prevent the query from being executed. ```ts import { skipToken } from '@tanstack/react-query' const [name, setName] = useState() const result = usePost({ variables: id ? { id: id } : skipToken, }) // and for useQueries example const queries = useQueries({ queries: [usePost.getOptions(id ? { id: id } : skipToken)], }) ``` ## FAQ ### What is the difference between `getFetchOptions` and `getOptions`? `getFetchOptions` would only return necessary options, while options like `staleTime` and `retry` would be omited ### What is the difference between `fetcher` and `queryFn`? ReactQueryKit would automatically converts fetcher to queryFn, as shown below: ```ts const useTest = createQuery({ queryKey: ['test'], fetcher: (variables, context) => { // ... }, }) // => useTest.getOptions(variables): // { // queryKey: ['test', variables], // queryFn: (context) => fetcher(variables, context) // } ``` ## Migration Upgrading from ReactQueryKit 2 → ReactQueryKit 3 ```diff createQuery({ - primaryKey: 'posts', - queryFn: ({ queryKey: [_primaryKey, variables] }) => {}, + queryKey: ['posts'], + fetcher: variables => {}, }) ``` What you benefit from ReactQueryKit 3 - Support hierarchical key - Support infer the types of fetcher, you can enjoy the preferred types automatically. - Support to create a shape of your entire API ## Issues _Looking to contribute? Look for the [Good First Issue][good-first-issue] label._ ### 🐛 Bugs Please file an issue for bugs, missing documentation, or unexpected behavior. [**See Bugs**][bugs] ### 💡 Feature Requests Please file an issue to suggest new features. Vote on feature requests by adding a 👍. This helps maintainers prioritize what to work on. [**See Feature Requests**][requests] ## LICENSE MIT [npm]: https://www.npmjs.com [node]: https://nodejs.org [bugs]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug [requests]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement [good-first-issue]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22 ================================================ FILE: babel.config.js ================================================ module.exports = { presets: [ [ '@babel/preset-env', { loose: true, modules: false, exclude: [ '@babel/plugin-transform-regenerator', '@babel/plugin-transform-parameters', ], }, ], '@babel/preset-typescript', ], } ================================================ FILE: jest.config.js ================================================ /** @type {import('jest').Config} */ const config = { transform: { '\\.[jt]sx?$': 'ts-jest', }, testEnvironment: 'jsdom', } module.exports = config ================================================ FILE: package.json ================================================ { "name": "react-query-kit", "version": "3.3.3", "description": "🕊️ A toolkit for ReactQuery that make ReactQuery hooks more reusable and typesafe", "author": "liaoliao666", "repository": "liaoliao666/react-query-kit", "homepage": "https://github.com/liaoliao666/react-query-kit#readme", "types": "build/lib/index.d.ts", "main": "build/lib/index.js", "module": "build/lib/index.esm.js", "exports": { ".": { "types": "./build/lib/index.d.ts", "import": "./build/lib/index.mjs", "default": "./build/lib/index.js" }, "./package.json": "./package.json" }, "license": "MIT", "devDependencies": { "@babel/core": "^7.18.10", "@babel/preset-env": "^7.18.10", "@babel/preset-typescript": "^7.18.6", "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", "@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-commonjs": "^22.0.2", "@rollup/plugin-node-resolve": "^13.2.1", "@rollup/plugin-replace": "^4.0.0", "@tanstack/react-query": "^5.100.6", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@trivago/prettier-plugin-sort-imports": "^4.2.0", "@types/jest": "^28.1.6", "@typescript-eslint/eslint-plugin": "^5.32.0", "@typescript-eslint/parser": "^5.32.0", "eslint": "^8.21.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "^26.8.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", "husky": "^8.0.1", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "prettier": "^2.7.1", "react": "^18.2.0", "react-dom": "^18.2.0", "replace": "^1.2.1", "rollup": "^2.77.2", "rollup-plugin-size": "^0.2.2", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-visualizer": "^5.7.1", "ts-jest": "^29.1.0", "typescript": "^5.1.6" }, "peerDependencies": { "@tanstack/react-query": "^4 || ^5" }, "peerDependenciesMeta": { "@tanstack/react-query": { "optional": true } }, "sideEffects": false, "scripts": { "build": "rollup --config rollup.config.js && npm run typecheck", "typecheck": "tsc -b && tsc -p tsconfig.types.json --pretty false", "stats": "open ./build/stats-html.html", "eslint": "eslint --fix '*.{js,json}' '{src,tests,benchmarks}/**/*.{ts,tsx}'", "test": "jest" }, "dependencies": {}, "files": [ "build/*", "src" ], "keywords": [ "react", "react-query" ] } ================================================ FILE: prettier.config.js ================================================ module.exports = { printWidth: 80, tabWidth: 2, useTabs: false, semi: false, singleQuote: true, trailingComma: 'es5', bracketSpacing: true, jsxBracketSameLine: false, arrowParens: 'avoid', endOfLine: 'auto', plugins: [require('@trivago/prettier-plugin-sort-imports')], importOrder: ['^[./]'], importOrderSeparation: true, importOrderSortSpecifiers: true, } ================================================ FILE: rollup.config.js ================================================ import { babel } from '@rollup/plugin-babel' import commonJS from '@rollup/plugin-commonjs' import { nodeResolve } from '@rollup/plugin-node-resolve' import replace from '@rollup/plugin-replace' import size from 'rollup-plugin-size' import { terser } from 'rollup-plugin-terser' import visualizer from 'rollup-plugin-visualizer' const replaceDevPlugin = type => replace({ 'process.env.NODE_ENV': `"${type}"`, delimiters: ['', ''], preventAssignment: true, }) const extensions = ['.ts', '.tsx'] const babelPlugin = babel({ babelHelpers: 'bundled', exclude: /node_modules/, extensions, }) export default function rollup() { const options = { input: 'src/index.ts', jsName: 'ReactQueryKit', external: ['@tanstack/react-query'], globals: { '@tanstack/react-query': 'ReactQuery', }, } return [ mjs(options), esm(options), cjs(options), umdDev(options), umdProd(options), ] } function mjs({ input, external }) { return { // ESM external, input, output: { format: 'esm', sourcemap: true, dir: `build/lib`, preserveModules: true, entryFileNames: '[name].mjs', }, plugins: [babelPlugin, commonJS(), nodeResolve({ extensions })], } } function esm({ input, external }) { return { // ESM external, input, output: { format: 'esm', dir: `build/lib`, sourcemap: true, preserveModules: true, entryFileNames: '[name].esm.js', }, plugins: [babelPlugin, commonJS(), nodeResolve({ extensions })], } } function cjs({ input, external }) { return { // CJS external, input, output: { format: 'cjs', sourcemap: true, dir: `build/lib`, preserveModules: true, exports: 'named', entryFileNames: '[name].js', }, plugins: [babelPlugin, commonJS(), nodeResolve({ extensions })], } } function umdDev({ input, external, globals, jsName }) { return { // UMD (Dev) external, input, output: { format: 'umd', sourcemap: true, file: `build/umd/index.development.js`, name: jsName, globals, }, plugins: [ babelPlugin, commonJS(), nodeResolve({ extensions }), replaceDevPlugin('development'), ], } } function umdProd({ input, external, globals, jsName }) { return { // UMD (Prod) external, input, output: { format: 'umd', sourcemap: true, file: `build/umd/index.production.js`, name: jsName, globals, }, plugins: [ babelPlugin, commonJS(), nodeResolve({ extensions }), replaceDevPlugin('production'), terser({ mangle: true, compress: true, }), size({}), visualizer({ filename: `build/stats-html.html`, gzipSize: true, }), visualizer({ filename: `build/stats.json`, json: true, gzipSize: true, }), ], } } ================================================ FILE: src/createBaseQuery.ts ================================================ import { type QueryClient, type QueryFunctionContext, type UseBaseQueryOptions, type UseInfiniteQueryOptions, } from '@tanstack/react-query' import { ReactQuery, getKey as getFullKey, withMiddleware } from './utils' type QueryBaseHookOptions = Omit< UseBaseQueryOptions, 'queryKey' | 'queryFn' > & { fetcher?: any variables?: any } export const createBaseQuery = ( defaultOptions: any, useRQHook: (options: any, queryClient?: any) => any, overrideOptions?: Partial ): any => { if (process.env.NODE_ENV !== 'production') { // @ts-ignore if (defaultOptions.useDefaultOptions) { console.error( '[Bug] useDefaultOptions is not supported, please use middleware instead.' ) } // @ts-ignore if (defaultOptions.queryFn) { console.error( '[Bug] queryFn is not supported, please use fetcher instead.' ) } } const getQueryOptions = (fetcherFn: any, variables: any) => { return { queryFn: variables && variables === ReactQuery.skipToken ? ReactQuery.skipToken : (context: QueryFunctionContext) => fetcherFn(variables, context), queryKey: getFullKey(defaultOptions.queryKey, variables), } } const getKey = (variables?: any) => getFullKey(defaultOptions.queryKey, variables) const getOptions = (variables: any) => { return { ...defaultOptions, ...getQueryOptions(defaultOptions.fetcher, variables), } } const getFetchOptions = (variables: any) => { return { ...getQueryOptions(defaultOptions.fetcher, variables), queryKeyHashFn: defaultOptions.queryKeyHashFn, getPreviousPageParam: defaultOptions.getPreviousPageParam, getNextPageParam: defaultOptions.getNextPageParam, initialPageParam: defaultOptions.initialPageParam, } } const useBaseHook = ( options: QueryBaseHookOptions, queryClient?: QueryClient ) => { return useRQHook( { ...options, ...getQueryOptions(options.fetcher, options.variables), ...overrideOptions, }, queryClient ) } return Object.assign(withMiddleware(useBaseHook, defaultOptions, 'queries'), { fetcher: defaultOptions.fetcher, getKey, getOptions, getFetchOptions, }) } ================================================ FILE: src/createInfiniteQuery.ts ================================================ import { createBaseQuery } from './createBaseQuery' import type { CompatibleError, CreateInfiniteQueryOptions, InfiniteQueryHook, } from './types' import { ReactQuery } from './utils' export function createInfiniteQuery< TFnData, TVariables = void, TError = CompatibleError, TPageParam = number >( options: CreateInfiniteQueryOptions ): InfiniteQueryHook { return createBaseQuery(options, ReactQuery.useInfiniteQuery) } ================================================ FILE: src/createMutation.ts ================================================ import type { CompatibleError, CreateMutationOptions, MutationHook, } from './types' import { ReactQuery, withMiddleware } from './utils' export function createMutation< TData = unknown, TVariables = void, TError = CompatibleError, TContext = unknown >( defaultOptions: CreateMutationOptions ): MutationHook { return Object.assign( withMiddleware(ReactQuery.useMutation, defaultOptions, 'mutations'), { getKey: () => defaultOptions.mutationKey, getOptions: () => defaultOptions, mutationFn: defaultOptions.mutationFn, } ) as MutationHook } ================================================ FILE: src/createQuery.ts ================================================ import { createBaseQuery } from './createBaseQuery' import type { CompatibleError, CreateQueryOptions, QueryHook } from './types' import { ReactQuery } from './utils' export function createQuery< TFnData, TVariables = void, TError = CompatibleError >( options: CreateQueryOptions ): QueryHook { return createBaseQuery(options, ReactQuery.useQuery) } ================================================ FILE: src/createSuspenseInfiniteQuery.ts ================================================ import { createBaseQuery } from './createBaseQuery' import type { CompatibleError, CreateSuspenseInfiniteQueryOptions, SuspenseInfiniteQueryHook, } from './types' import { ReactQuery, isV5, suspenseOptions } from './utils' export function createSuspenseInfiniteQuery< TFnData, TVariables = void, TError = CompatibleError, TPageParam = number >( options: CreateSuspenseInfiniteQueryOptions< TFnData, TVariables, TError, TPageParam > ): SuspenseInfiniteQueryHook { return isV5 ? createBaseQuery(options, ReactQuery.useSuspenseInfiniteQuery) : createBaseQuery(options, ReactQuery.useInfiniteQuery, suspenseOptions) } ================================================ FILE: src/createSuspenseQuery.ts ================================================ import { createBaseQuery } from './createBaseQuery' import type { CompatibleError, CreateSuspenseQueryOptions, SuspenseQueryHook, } from './types' import { ReactQuery, isV5, suspenseOptions } from './utils' export function createSuspenseQuery< TFnData, TVariables = void, TError = CompatibleError >( options: CreateSuspenseQueryOptions ): SuspenseQueryHook { return isV5 ? createBaseQuery(options, ReactQuery.useSuspenseQuery) : createBaseQuery(options, ReactQuery.useQuery, suspenseOptions) } ================================================ FILE: src/index.ts ================================================ export * from './createQuery' export * from './createSuspenseQuery' export * from './createInfiniteQuery' export * from './createSuspenseInfiniteQuery' export * from './createMutation' export * from './types' export * from './router' export { getKey } from './utils' ================================================ FILE: src/router.ts ================================================ import { QueryKey } from '@tanstack/react-query' import { createInfiniteQuery } from './createInfiniteQuery' import { createMutation } from './createMutation' import { createQuery } from './createQuery' import { createSuspenseInfiniteQuery } from './createSuspenseInfiniteQuery' import { createSuspenseQuery } from './createSuspenseQuery' import type { CompatibleError, CreateRouter, RouterConfig, RouterInfiniteQuery, RouterInfiniteQueryOptions, RouterMutation, RouterMutationOptions, RouterQuery, RouterQueryOptions, } from './types' type RouterNode = RouterConfig[string] type RouterTypedNode = RouterNode & { _routerType: 'q' | 'inf' | 'm' } const isRouterLeaf = (value: RouterNode): value is RouterTypedNode => { return !!value && typeof value._routerType === 'string' } const buildRouter = (keys: QueryKey, config: RouterConfig) => { return Object.entries(config).reduce( (acc, [key, opts]) => { if (!isRouterLeaf(opts)) { acc[key] = buildRouter([...keys, key], opts) } else { const type = opts._routerType const options: any = { ...opts, [type === `m` ? `mutationKey` : `queryKey`]: [...keys, key], } if (type === `m`) { acc[key] = { useMutation: createMutation(options), ...createMutation(options), } return acc } acc[key] = type === `q` ? { useQuery: createQuery(options), useSuspenseQuery: createSuspenseQuery(options), ...createQuery(options), } : { useInfiniteQuery: createInfiniteQuery(options), useSuspenseInfiniteQuery: createSuspenseInfiniteQuery(options), ...createInfiniteQuery(options), } } return acc }, { getKey: () => keys, } as any ) } export const router = ( key: string | QueryKey, config: TConfig ): CreateRouter => { return buildRouter(Array.isArray(key) ? key : [key], config) } function query( options: RouterQueryOptions ): RouterQuery { return { ...options, _routerType: 'q', } } function infiniteQuery< TFnData, TVariables = void, TError = CompatibleError, TPageParam = number >( options: RouterInfiniteQueryOptions ): RouterInfiniteQuery { return { ...options, _routerType: 'inf' } as RouterInfiniteQuery< TFnData, TVariables, TError, TPageParam > } function mutation< TFnData = unknown, TVariables = void, TError = CompatibleError, TContext = unknown >( options: RouterMutationOptions ): RouterMutation { return { ...options, _routerType: 'm' } as RouterMutation< TFnData, TVariables, TError, TContext > } router.query = query router.infiniteQuery = infiniteQuery router.mutation = mutation ================================================ FILE: src/types.ts ================================================ import type { DataTag, DefaultError, DefinedUseInfiniteQueryResult, DefinedUseQueryResult, InfiniteData, InfiniteQueryObserverSuccessResult, MutationFunctionContext, MutationKey, QueryClient, QueryFunction, QueryFunctionContext, QueryKey, QueryKeyHashFunction, QueryObserverSuccessResult, SkipToken, UseInfiniteQueryOptions, UseInfiniteQueryResult, UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult, } from '@tanstack/react-query' // utils type CompatibleWithV4 = InfiniteData extends UseInfiniteQueryResult< InfiniteData >['data'] ? V5 : V4 type CompatibleWithTwoV5 = UseInfiniteQueryOptions< unknown, DefaultError, unknown, QueryKey, unknown > extends never ? V5Generics : V6Generics type CompatibleUseInfiniteQueryOptions = CompatibleWithV4< CompatibleWithTwoV5< UseInfiniteQueryOptions, // @ts-ignore UseInfiniteQueryOptions< TFnData, TError, TData, TFnData, QueryKey, TPageParam > >, // @ts-ignore UseInfiniteQueryOptions > type CompatibleInfiniteData = CompatibleWithV4< InfiniteData, InfiniteData > type NonUndefinedGuard = T extends undefined ? never : T type WithRequired = T & { [_ in K]: {} } type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial } : T type DefaultTo = unknown extends T ? D : T export type CompatibleError = CompatibleWithV4 export type Fetcher = ( variables: TVariables, context: QueryFunctionContext ) => TFnData | Promise export type AdditionalQueryOptions = { fetcher: Fetcher variables?: TVariables } type inferMiddlewareHook any> = ( options: inferCreateOptions, queryClient?: CompatibleWithV4 ) => ReturnType export type Middleware< T extends (...args: any) => any = QueryHook > = (hook: inferMiddlewareHook) => inferMiddlewareHook export type ExposeFetcher = ( variables: TVariables, context?: Partial> ) => TFnData | Promise export type ExposeMethods = { fetcher: ExposeFetcher getKey: ( variables?: DeepPartial ) => CompatibleWithV4< DataTag< QueryKey, [TPageParam] extends [never] ? TFnData : InfiniteData >, QueryKey > getFetchOptions: ( variables: TVariables extends void ? CompatibleWithV4 | void : CompatibleWithV4 ) => Pick< ReturnType< ExposeMethods['getOptions'] >, // @ts-ignore [TPageParam] extends [never] ? 'queryKey' | 'queryFn' | 'queryKeyHashFn' : | 'queryKey' | 'queryFn' | 'queryKeyHashFn' | 'getNextPageParam' | 'getPreviousPageParam' | 'initialPageParam' > getOptions: ( variables: TVariables extends void ? CompatibleWithV4 | void : CompatibleWithV4 ) => [TPageParam] extends [never] ? CompatibleWithV4< UseQueryOptions & { queryKey: DataTag queryFn?: Exclude< UseQueryOptions['queryFn'], SkipToken > }, // Not work to infer TError in v4 { queryKey: QueryKey queryFn: QueryFunction queryKeyHashFn?: QueryKeyHashFunction } > : CompatibleUseInfiniteQueryOptions< TFnData, TFnData, TError, TPageParam > & { queryKey: CompatibleWithV4< DataTag>, QueryKey > } } type Clone = T extends infer TClone ? TClone : never // query hook export interface CreateQueryOptions< TFnData = unknown, TVariables = void, TError = CompatibleError > extends Omit< UseQueryOptions, 'queryKey' | 'queryFn' | 'select' >, AdditionalQueryOptions { queryKey: QueryKey use?: Middleware< QueryHook, Clone, Clone> >[] variables?: TVariables } export interface QueryHookOptions extends Omit< UseQueryOptions, 'queryKey' | 'queryFn' | 'queryKeyHashFn' > { use?: Middleware>[] variables?: CompatibleWithV4 } export interface DefinedQueryHookOptions extends Omit< QueryHookOptions, 'initialData' > { initialData: NonUndefinedGuard | (() => NonUndefinedGuard) } export type QueryHookResult = UseQueryResult export type DefinedQueryHookResult = DefinedUseQueryResult< TData, TError > export interface QueryHook< TFnData = unknown, TVariables = void, TError = CompatibleError > extends ExposeMethods { ( options: DefinedQueryHookOptions, queryClient?: CompatibleWithV4 ): DefinedQueryHookResult ( options?: QueryHookOptions, queryClient?: CompatibleWithV4 ): QueryHookResult } // suspense query hook export interface CreateSuspenseQueryOptions< TFnData = unknown, TVariables = void, TError = CompatibleError > extends Omit< UseQueryOptions, | 'queryKey' | 'queryFn' | 'enabled' | 'select' | 'suspense' | 'throwOnError' | 'placeholderData' | 'keepPreviousData' | 'useErrorBoundary' >, AdditionalQueryOptions { queryKey: QueryKey use?: Middleware< SuspenseQueryHook, Clone, Clone> >[] variables?: TVariables } export interface SuspenseQueryHookOptions extends Omit< UseQueryOptions, | 'queryKey' | 'queryFn' | 'queryKeyHashFn' | 'enabled' | 'suspense' | 'throwOnError' | 'placeholderData' | 'keepPreviousData' | 'useErrorBoundary' > { use?: Middleware>[] variables?: CompatibleWithV4 } export type SuspenseQueryHookResult = Omit< QueryObserverSuccessResult, 'isPlaceholderData' | 'isPreviousData' > export interface SuspenseQueryHook< TFnData = unknown, TVariables = void, TError = CompatibleError > extends ExposeMethods { ( options?: SuspenseQueryHookOptions, queryClient?: CompatibleWithV4 ): SuspenseQueryHookResult } // infinite query hook export interface CreateInfiniteQueryOptions< TFnData = unknown, TVariables = void, TError = CompatibleError, TPageParam = number > extends Omit< CompatibleUseInfiniteQueryOptions, 'queryKey' | 'queryFn' | 'select' >, AdditionalQueryOptions { queryKey: QueryKey use?: Middleware< InfiniteQueryHook< Clone, Clone, Clone, Clone > >[] variables?: TVariables } export interface InfiniteQueryHookOptions< TFnData, TError, TData, TVariables, TPageParam = number > extends Omit< CompatibleUseInfiniteQueryOptions, | 'queryKey' | 'queryFn' | 'queryKeyHashFn' | 'initialPageParam' | 'getPreviousPageParam' | 'getNextPageParam' > { use?: Middleware>[] variables?: CompatibleWithV4 } export interface DefinedInfiniteQueryHookOptions< TFnData, TError, TData, TVariables, TPageParam = number > extends Omit< InfiniteQueryHookOptions, 'initialData' > { initialData: | NonUndefinedGuard> | (() => NonUndefinedGuard>) } export type InfiniteQueryHookResult = UseInfiniteQueryResult< TData, TError > export type DefinedInfiniteQueryHookResult = CompatibleWithV4< DefinedUseInfiniteQueryResult, WithRequired, 'data'> > export interface InfiniteQueryHook< TFnData = unknown, TVariables = void, TError = CompatibleError, TPageParam = number > extends ExposeMethods { , TFnData>>( options: DefinedInfiniteQueryHookOptions< TFnData, TError, TData, TVariables, TPageParam >, queryClient?: CompatibleWithV4 ): DefinedInfiniteQueryHookResult , TFnData>>( options?: InfiniteQueryHookOptions< TFnData, TError, TData, TVariables, TPageParam >, queryClient?: CompatibleWithV4 ): InfiniteQueryHookResult } // infinite sususpense query hook export interface CreateSuspenseInfiniteQueryOptions< TFnData = unknown, TVariables = void, TError = CompatibleError, TPageParam = number > extends Omit< CompatibleUseInfiniteQueryOptions, | 'queryKey' | 'queryFn' | 'enabled' | 'select' | 'suspense' | 'throwOnError' | 'placeholderData' | 'keepPreviousData' | 'useErrorBoundary' >, AdditionalQueryOptions { queryKey: QueryKey use?: Middleware< SuspenseInfiniteQueryHook< Clone, Clone, Clone, Clone > >[] variables?: TVariables } export interface SuspenseInfiniteQueryHookOptions< TFnData, TError, TData, TVariables, TPageParam = number > extends Omit< CompatibleUseInfiniteQueryOptions, | 'queryKey' | 'queryFn' | 'queryKeyHashFn' | 'enabled' | 'initialPageParam' | 'getPreviousPageParam' | 'getNextPageParam' | 'suspense' | 'throwOnError' | 'placeholderData' | 'keepPreviousData' | 'useErrorBoundary' > { use?: Middleware>[] variables?: CompatibleWithV4 } export type SuspenseInfiniteQueryHookResult = Omit< InfiniteQueryObserverSuccessResult, 'isPlaceholderData' | 'isPreviousData' > export interface SuspenseInfiniteQueryHook< TFnData = unknown, TVariables = void, TError = CompatibleError, TPageParam = number > extends ExposeMethods { , TFnData>>( options?: SuspenseInfiniteQueryHookOptions< TFnData, TError, TData, TVariables, TPageParam >, queryClient?: CompatibleWithV4 ): SuspenseInfiniteQueryHookResult } // mutation hook export interface CreateMutationOptions< TData = unknown, TVariables = void, TError = CompatibleError, TContext = unknown > extends UseMutationOptions { use?: Middleware>[] } export interface MutationHookOptions extends Omit< UseMutationOptions, 'mutationFn' | 'mutationKey' > { use?: Middleware>[] } export type MutationHookResult< TData = unknown, TError = CompatibleError, TVariables = void, TContext = unknown > = UseMutationResult export type ExposeMutationFn = [ TVariables ] extends [void] ? ( variables?: TVariables, context?: Partial ) => Promise : ( variables: TVariables, context?: Partial ) => Promise export interface ExposeMutationMethods< TData = unknown, TVariables = void, TError = CompatibleError, TDefaultContext = unknown > { getKey: () => MutationKey | undefined getOptions: () => UseMutationOptions< TData, TError, TVariables, TDefaultContext > mutationFn: ExposeMutationFn } export interface MutationHook< TData = unknown, TVariables = void, TError = CompatibleError, TDefaultContext = unknown > extends ExposeMutationMethods { ( options?: MutationHookOptions, queryClient?: CompatibleWithV4 ): MutationHookResult } // infer types export type inferVariables = T extends { fetcher: ExposeFetcher } ? TVariables : T extends ExposeMutationMethods ? TVariables : never export type inferData = T extends { fetcher: ExposeFetcher } ? [TPageParam] extends [never] ? TFnData : CompatibleInfiniteData : T extends ExposeMutationMethods ? TFnData : never export type inferFnData = T extends { fetcher: ExposeFetcher } ? TFnData : T extends ExposeMutationMethods ? TFnData : never export type inferError = T extends ExposeMethods ? TError : T extends ExposeMethods ? TError : T extends ExposeMutationMethods ? TError : never export type inferOptions = T extends QueryHook< infer TFnData, infer TVariables, infer TError > ? QueryHookOptions : T extends SuspenseQueryHook ? SuspenseQueryHookOptions : T extends InfiniteQueryHook< infer TFnData, infer TVariables, infer TError, infer TPageParam > ? InfiniteQueryHookOptions< TFnData, TError, CompatibleWithV4, TFnData>, TVariables, TPageParam > : T extends SuspenseInfiniteQueryHook< infer TFnData, infer TVariables, infer TError, infer TPageParam > ? SuspenseInfiniteQueryHookOptions< TFnData, TError, CompatibleWithV4, TFnData>, TVariables, TPageParam > : T extends MutationHook ? MutationHookOptions : never export type inferCreateOptions = T extends QueryHook< infer TFnData, infer TVariables, infer TError > ? CreateQueryOptions : T extends SuspenseQueryHook ? CreateSuspenseQueryOptions : T extends InfiniteQueryHook< infer TFnData, infer TVariables, infer TError, infer TPageParam > ? CreateInfiniteQueryOptions : T extends SuspenseInfiniteQueryHook< infer TFnData, infer TVariables, infer TError, infer TPageParam > ? CreateSuspenseInfiniteQueryOptions : T extends MutationHook< infer TFnData, infer TVariables, infer TError, infer TContext > ? CreateMutationOptions : never // router export type RouterQueryOptions< TFnData, TVariables = void, TError = CompatibleError > = Omit, 'queryKey'> export type RouterQuery< TFnData, TVariables = void, TError = CompatibleError > = RouterQueryOptions & { _routerType: `q` } export type ResolvedRouterQuery< TFnData, TVariables = void, TError = CompatibleError > = { useQuery: QueryHook useSuspenseQuery: SuspenseQueryHook } & ExposeMethods export type RouterInfiniteQueryOptions< TFnData, TVariables = void, TError = CompatibleError, TPageParam = number > = Omit< CreateInfiniteQueryOptions, 'queryKey' > export type RouterInfiniteQuery< TFnData, TVariables = void, TError = CompatibleError, TPageParam = number > = RouterInfiniteQueryOptions< TFnData, TVariables, TError, Clone > & { _routerType: `inf` } export type ResolvedRouterInfiniteQuery< TFnData, TVariables = void, TError = CompatibleError, TPageParam = number > = { useInfiniteQuery: InfiniteQueryHook useSuspenseInfiniteQuery: SuspenseInfiniteQueryHook< TFnData, TVariables, TError, TPageParam > } & ExposeMethods export type RouterMutationOptions< TData = unknown, TVariables = void, TError = CompatibleError, TContext = unknown > = Omit< CreateMutationOptions, 'mutationKey' > export type RouterMutation< TData = unknown, TVariables = void, TError = CompatibleError, TContext = unknown > = RouterMutationOptions & { _routerType: `m` } export type ResolvedRouterMutation< TData = unknown, TVariables = void, TError = CompatibleError, TContext = unknown > = { useMutation: MutationHook< TData, DefaultTo, DefaultTo, TContext > } & ExposeMutationMethods< TData, DefaultTo, DefaultTo, TContext > export type RouterLeaf = | RouterQuery | RouterInfiniteQuery | RouterMutation export type RouterConfig = { _routerType?: never } & { [k: string]: RouterLeaf | RouterConfig } export type CreateRouter = { [K in keyof TConfig]: TConfig[K] extends RouterMutation< infer TFnData, infer TVariables, infer TError, infer TContext > ? ResolvedRouterMutation< TFnData, DefaultTo, DefaultTo, TContext > : TConfig[K] extends RouterInfiniteQuery< infer TFnData, infer TVariables, infer TError, infer TPageParam > ? ResolvedRouterInfiniteQuery< TFnData, DefaultTo, DefaultTo, DefaultTo > : TConfig[K] extends RouterQuery< infer TFnData, infer TVariables, infer TError > ? ResolvedRouterQuery< TFnData, DefaultTo, DefaultTo > : TConfig[K] extends RouterConfig ? CreateRouter : never } & { getKey: () => QueryKey } ================================================ FILE: src/utils.ts ================================================ import * as TanstackReactQuery from '@tanstack/react-query' import type { Query, QueryClient, QueryKey } from '@tanstack/react-query' import type { Middleware } from './types' export const ReactQuery = TanstackReactQuery export const isV5 = !!ReactQuery.useSuspenseQuery export const suspenseOptions = { enabled: true, suspense: true, keepPreviousData: undefined, useErrorBoundary: (_error: unknown, query: Query) => query.state.data === undefined, } export const withMiddleware = ( hook: any, defaultOptions: any, type: 'queries' | 'mutations' ) => { return function useMiddleware( options?: { client?: QueryClient; use?: Middleware[] }, queryClient?: QueryClient ) { const [uses, opts]: [Middleware[], any] = [ ReactQuery.useQueryClient( // @ts-ignore Compatible with ReactQuery v4 isV5 ? queryClient : options ).getDefaultOptions()[type], defaultOptions, options, ].reduce( ([u1, o1], { use: u2 = [], ...o2 } = {}) => [ [...u1, ...u2], { ...o1, ...o2 }, ], [[]] ) return uses.reduceRight((next, use) => use(next), hook)(opts, queryClient) } } export const getKey = (queryKey: QueryKey, variables?: any): QueryKey => { return variables === undefined ? queryKey : [...queryKey, variables] } ================================================ FILE: tests/createInfiniteQuery.test.tsx ================================================ import { createInfiniteQuery } from '../src/createInfiniteQuery' import { omit, uniqueKey } from './utils' describe('createInfiniteQuery', () => { it('should return the correct key', () => { type Response = { projects: { id: string; name: string }[] nextCursor: number } type Variables = { id: number } const key = uniqueKey() const variables = { id: 1 } const fetcher = (_variables: Variables): Promise => { return fetch(`/test`).then(res => res.json()) } const initialPageParam = 1 const getNextPageParam = (lastPage: Response) => lastPage.nextCursor const useGeneratedQuery = createInfiniteQuery({ queryKey: key, fetcher, initialPageParam, getNextPageParam, }) expect(useGeneratedQuery.getKey()).toEqual(key) expect(useGeneratedQuery.getKey(variables)).toEqual([...key, variables]) expect(omit(useGeneratedQuery.getOptions(variables), 'queryFn')).toEqual({ queryKey: [...key, variables], fetcher, initialPageParam, getNextPageParam, }) expect( omit(useGeneratedQuery.getFetchOptions(variables), 'queryFn') ).toEqual({ queryKey: [...key, variables], initialPageParam, getNextPageParam, }) }) }) ================================================ FILE: tests/createMutation.test.tsx ================================================ import { type MutationKey } from '@tanstack/react-query' import { createMutation } from '../src/createMutation' describe('createMutation', () => { it('should return the correct key', () => { const mutationKey: MutationKey = ['mutationKey'] const mutation = createMutation({ mutationKey, mutationFn: async () => mutationKey, }) expect(mutation.getKey()).toEqual(mutationKey) mutation.mutationFn().then(data => expect(data).toEqual(mutationKey)) }) }) ================================================ FILE: tests/createQuery.test.tsx ================================================ import { QueryClient, skipToken } from '@tanstack/react-query' import '@testing-library/jest-dom' import { fireEvent, waitFor } from '@testing-library/react' import * as React from 'react' import { createQuery } from '../src' import type { QueryHookResult } from '../src' import { Middleware } from '../src/types' import { omit, renderWithClient, sleep, uniqueKey } from './utils' describe('createQuery', () => { const queryClient = new QueryClient() it('should return the correct key', () => { const key = uniqueKey() const variables = { id: 1 } const fetcher = (_variables: { id: number }) => { return 'test' } const useGeneratedQuery = createQuery({ queryKey: key, fetcher, }) expect(useGeneratedQuery.getKey()).toEqual(key) expect(useGeneratedQuery.getKey(variables)).toEqual([...key, variables]) expect(omit(useGeneratedQuery.getOptions(variables), 'queryFn')).toEqual({ queryKey: [...key, variables], fetcher, }) expect( omit(useGeneratedQuery.getFetchOptions(variables), 'queryFn') ).toEqual({ queryKey: [...key, variables], }) queryClient.prefetchQuery(useGeneratedQuery.getFetchOptions(variables)) }) it('should return the correct initial data from middleware', async () => { const myMiddileware: Middleware = useQueryNext => { return options => { return useQueryNext({ ...options, initialData: 'initialData', enabled: false, }) } } const useGeneratedQuery = createQuery({ queryKey: uniqueKey(), fetcher: (_variables: { id: number }) => { return 'test' }, use: [ useNext => { return options => useNext({ ...options, initialData: 'fakeData', enabled: false, }) }, myMiddileware, ], }) const states: QueryHookResult[] = [] function Page() { const state = useGeneratedQuery() states.push(state) return {state.data} } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('initialData')) }) it('should return the correct initial data', async () => { const useGeneratedQuery = createQuery({ queryKey: uniqueKey(), fetcher: () => { return 'test' }, use: [ useNext => { return options => useNext({ ...options, initialData: options.initialData ?? 'initialData', enabled: false, }) }, ], }) const states: QueryHookResult[] = [] function Page() { const state = useGeneratedQuery({ initialData: 'stateData' }) states.push(state) return {state.data} } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('stateData')) }) it('should return the selected data', async () => { const useGeneratedQuery = createQuery({ queryKey: uniqueKey(), fetcher: () => { return 'test' }, }) const states: QueryHookResult[] = [] function Page() { const state = useGeneratedQuery({ select() { return 'selectedData' }, }) states.push(state) return {state.data} } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('selectedData')) }) it('should respect skipToken and refetch when skipToken is taken away', async () => { const useGeneratedQuery = createQuery({ queryKey: uniqueKey(), fetcher: async () => { await sleep(10) return Promise.resolve('data') }, }) function Page({ enabled }: { enabled: boolean }) { const { data, status } = useGeneratedQuery({ variables: enabled ? undefined : skipToken, retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }) return (
status: {status}
data: {String(data)}
) } function App() { const [enabled, toggle] = React.useReducer(x => !x, false) return (
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: pending')) fireEvent.click(rendered.getByRole('button', { name: 'enable' })) await waitFor(() => rendered.getByText('status: success')) await waitFor(() => rendered.getByText('data: data')) }) }) ================================================ FILE: tests/router.test.tsx ================================================ import { router } from '../src' describe('router', () => { it('should return the correct shape', () => { const post = router(`post`, { byId: router.query({ fetcher: (variables: { id: number }): Promise<{ title: string; content: string }> => fetch(`/post/${variables.id}`).then(res => res.json()), }), list: router.infiniteQuery({ fetcher: (_variables, { pageParam }) => fetch(`/post/?cursor=${pageParam}`).then(res => res.json()), getNextPageParam: lastPage => lastPage.nextCursor, initialPageParam: 0, }), add: router.mutation({ mutationFn: async (variables: { title: string content: string }): Promise<{ ret: number }> => fetch('/post', { method: 'POST', body: JSON.stringify(variables), }).then(res => res.json()), }), command: { report: router.query({ fetcher: (variables: { id: number }): Promise<{ title: string; content: string }> => fetch(`/post/report/${variables.id}`).then(res => res.json()), }), }, }) expect(post.getKey()).toEqual(['post']) expect(post.byId.getKey()).toEqual(['post', 'byId']) expect(post.byId.getKey({ id: 1 })).toEqual(['post', 'byId', { id: 1 }]) expect(post.list.getKey()).toEqual(['post', 'list']) expect(post.add.getKey()).toEqual(['post', 'add']) expect(post.command.getKey()).toEqual(['post', 'command']) expect(post.command.report.getKey()).toEqual(['post', 'command', 'report']) expect(post.command.report.getKey({ id: 1 })).toEqual([ 'post', 'command', 'report', { id: 1 }, ]) expect(typeof post.command.report.fetcher === 'function').toBe(true) expect(typeof post.command.report.getFetchOptions === 'function').toBe(true) expect(typeof post.command.report.getOptions === 'function').toBe(true) expect(typeof post.command.report.useQuery === 'function').toBe(true) expect(typeof post.command.report.useSuspenseQuery === 'function').toBe( true ) expect(typeof post.byId.fetcher === 'function').toBe(true) expect(typeof post.byId.getFetchOptions === 'function').toBe(true) expect(typeof post.byId.getOptions === 'function').toBe(true) expect(typeof post.byId.useQuery === 'function').toBe(true) expect(typeof post.byId.useSuspenseQuery === 'function').toBe(true) expect(typeof post.list.fetcher === 'function').toBe(true) expect(typeof post.list.getFetchOptions === 'function').toBe(true) expect(typeof post.list.getOptions === 'function').toBe(true) expect(typeof post.list.useInfiniteQuery === 'function').toBe(true) expect(typeof post.list.useSuspenseInfiniteQuery === 'function').toBe(true) expect(typeof post.add.mutationFn === 'function').toBe(true) expect(typeof post.add.getKey === 'function').toBe(true) expect(typeof post.add.getOptions === 'function').toBe(true) expect(typeof post.add.useMutation === 'function').toBe(true) }) it('should return the correct shape when pass a array keys', () => { const post = router(['scope', `post`], { byId: router.query({ fetcher: (variables: { id: number }): Promise<{ title: string; content: string }> => fetch(`/post/${variables.id}`).then(res => res.json()), }), list: router.infiniteQuery({ fetcher: (_variables, { pageParam }) => fetch(`/post/?cursor=${pageParam}`).then(res => res.json()), getNextPageParam: lastPage => lastPage.nextCursor, initialPageParam: 0, }), add: router.mutation({ mutationFn: async (variables: { title: string content: string }): Promise<{ ret: number }> => fetch('/post', { method: 'POST', body: JSON.stringify(variables), }).then(res => res.json()), }), command: { report: router.query({ fetcher: (variables: { id: number }): Promise<{ title: string; content: string }> => fetch(`/post/report/${variables.id}`).then(res => res.json()), }), }, }) expect(post.getKey()).toEqual(['scope', 'post']) expect(post.byId.getKey()).toEqual(['scope', 'post', 'byId']) expect(post.byId.getKey({ id: 1 })).toEqual([ 'scope', 'post', 'byId', { id: 1 }, ]) expect(post.list.getKey()).toEqual(['scope', 'post', 'list']) expect(post.add.getKey()).toEqual(['scope', 'post', 'add']) expect(post.command.getKey()).toEqual(['scope', 'post', 'command']) expect(post.command.report.getKey()).toEqual([ 'scope', 'post', 'command', 'report', ]) expect(post.command.report.getKey({ id: 1 })).toEqual([ 'scope', 'post', 'command', 'report', { id: 1 }, ]) expect(typeof post.command.report.fetcher === 'function').toBe(true) expect(typeof post.command.report.getFetchOptions === 'function').toBe(true) expect(typeof post.command.report.getOptions === 'function').toBe(true) expect(typeof post.command.report.useQuery === 'function').toBe(true) expect(typeof post.command.report.useSuspenseQuery === 'function').toBe( true ) expect(typeof post.byId.fetcher === 'function').toBe(true) expect(typeof post.byId.getFetchOptions === 'function').toBe(true) expect(typeof post.byId.getOptions === 'function').toBe(true) expect(typeof post.byId.useQuery === 'function').toBe(true) expect(typeof post.byId.useSuspenseQuery === 'function').toBe(true) expect(typeof post.list.fetcher === 'function').toBe(true) expect(typeof post.list.getFetchOptions === 'function').toBe(true) expect(typeof post.list.getOptions === 'function').toBe(true) expect(typeof post.list.useInfiniteQuery === 'function').toBe(true) expect(typeof post.list.useSuspenseInfiniteQuery === 'function').toBe(true) expect(typeof post.add.mutationFn === 'function').toBe(true) expect(typeof post.add.getKey === 'function').toBe(true) expect(typeof post.add.getOptions === 'function').toBe(true) expect(typeof post.add.useMutation === 'function').toBe(true) }) }) ================================================ FILE: tests/types.typecheck.ts ================================================ import { createQuery, router } from '../src' import type { Middleware } from '../src' const usePost = createQuery<{ title: string }, { id: number }>({ queryKey: ['post'], fetcher: async variables => ({ title: `post-${variables.id}`, }), }) const queryMiddleware: Middleware = useQueryNext => options => { options.fetcher options.variables?.id // @ts-expect-error Query middleware should not expose mutation options. options.mutationFn return useQueryNext(options) } void queryMiddleware type UsePostOptions = Parameters[0] const validQueryOptions: UsePostOptions = { variables: { id: 1 }, } void validQueryOptions const invalidQueryOptions: UsePostOptions = { // @ts-expect-error Query hooks should not accept mutation options. mutationFn: async () => ({ title: 'post-1' }), } void invalidQueryOptions const postRoutes = router('post', { byId: router.query({ fetcher: async (variables: { id: number }) => ({ id: variables.id, title: `post-${variables.id}`, }), }), add: router.mutation({ mutationFn: async (variables: { title: string }) => ({ title: variables.title, }), }), }) postRoutes.byId.useQuery({ variables: { id: 1 }, enabled: false, }) postRoutes.add.useMutation({ onSuccess(data) { const title: string = data.title void title }, }) // @ts-expect-error Query routes should not expose mutation hooks. postRoutes.byId.useMutation() // @ts-expect-error Mutation routes should not expose query hooks. postRoutes.add.useQuery() ================================================ FILE: tests/utils.tsx ================================================ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render } from '@testing-library/react' import * as React from 'react' let queryKeyCount = 0 export function uniqueKey(): string[] { queryKeyCount++ return [`query_${queryKeyCount}`] } export function renderWithClient( client: QueryClient, ui: React.ReactElement ): ReturnType { const { rerender, ...result } = render( {ui} ) return { ...result, rerender: (rerenderUi: React.ReactElement) => rerender( {rerenderUi} ), } as any } export function omit( object: T | null | undefined, ...paths: K ): Pick> { return Object.fromEntries( Object.entries(object || {}).filter(([key]) => !paths.includes(key)) ) as Pick> } export function sleep(timeout: number): Promise { return new Promise((resolve, _reject) => { setTimeout(resolve, timeout) }) } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2020"], "target": "ES2020", "module": "ES2020", "moduleResolution": "node", "allowSyntheticDefaultImports": true, "strict": true, "noImplicitAny": true, "noImplicitReturns": false, "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, "noUncheckedIndexedAccess": true, "strictNullChecks": true, "jsx": "react", "declaration": true, "emitDeclarationOnly": true, "esModuleInterop": true, "skipLibCheck": true, "types": ["jest", "node"], "outDir": "./build/lib" }, "files": ["src/index.ts"], "include": ["src"] } ================================================ FILE: tsconfig.types.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "declaration": false, "emitDeclarationOnly": false, "noEmit": true, "outDir": "./build/types" }, "files": ["tests/types.typecheck.ts"] }