├── .browserslistrc ├── .commitlintrc.json ├── .eslintrc ├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .husky └── commit-msg ├── .npmrc ├── .nvmrc ├── LICENSE ├── README-zh_CN.md ├── README.md ├── assets └── logo.svg ├── 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 └── utils.tsx ├── tsconfig.json ├── tsconfig.types.json └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers we support 2 | Chrome >= 73 3 | Firefox >= 78 4 | Edge >= 79 5 | Safari >= 12.0 6 | iOS >= 12.0 7 | opera >= 53 8 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "shared-node-browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "prettier", 12 | "plugin:prettier/recommended", 13 | "plugin:react-hooks/recommended", 14 | "plugin:import/errors", 15 | "plugin:import/warnings" 16 | ], 17 | "plugins": [ 18 | "@typescript-eslint", 19 | "react", 20 | "prettier", 21 | "react-hooks", 22 | "import", 23 | "jest" 24 | ], 25 | "parser": "@typescript-eslint/parser", 26 | "parserOptions": { 27 | "project": 2018, 28 | "sourceType": "module", 29 | "ecmaFeatures": { 30 | "jsx": true 31 | } 32 | }, 33 | "rules": { 34 | "eqeqeq": "error", 35 | "no-var": "error", 36 | "prefer-const": "error", 37 | "curly": ["warn", "multi-line", "consistent"], 38 | "no-console": "off", 39 | "@typescript-eslint/no-non-null-assertion": "off", 40 | "import/no-unresolved": ["error", { "commonjs": true, "amd": true }], 41 | "import/export": "error", 42 | "@typescript-eslint/ban-types": "off", 43 | "@typescript-eslint/no-duplicate-imports": ["error"], 44 | "@typescript-eslint/explicit-module-boundary-types": "off", 45 | "@typescript-eslint/no-unused-vars": [ 46 | "warn", 47 | { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } 48 | ], 49 | "@typescript-eslint/no-use-before-define": "off", 50 | "@typescript-eslint/no-empty-function": "off", 51 | "@typescript-eslint/no-explicit-any": "off", 52 | "@typescript-eslint/ban-ts-comment": "off", 53 | "jest/consistent-test-it": [ 54 | "error", 55 | { "fn": "it", "withinDescribe": "it" } 56 | ], 57 | "import/order": "off", 58 | "react/jsx-uses-react": "off", 59 | "react/react-in-jsx-scope": "off", 60 | "sort-imports": [ 61 | "error", 62 | { 63 | "ignoreDeclarationSort": true 64 | } 65 | ] 66 | }, 67 | "settings": { 68 | "react": { 69 | "version": "detect" 70 | }, 71 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"], 72 | "import/parsers": { 73 | "@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx"] 74 | }, 75 | "import/resolver": { 76 | "node": { 77 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], 78 | "paths": ["src"] 79 | } 80 | } 81 | }, 82 | "overrides": [ 83 | { 84 | "files": ["src"], 85 | "parserOptions": { 86 | "project": "./tsconfig.json" 87 | } 88 | }, 89 | { 90 | "files": ["tests/**/*.tsx"], 91 | "env": { 92 | "jest/globals": true 93 | } 94 | }, 95 | { 96 | "files": ["./*.js"], 97 | "rules": { 98 | "@typescript-eslint/no-var-requires": "off" 99 | } 100 | } 101 | ] 102 | } 103 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: 'publish' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: publish 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 16 17 | registry-url: https://registry.npmjs.org 18 | cache: 'yarn' 19 | - run: | 20 | git config --global user.name 'liaoliao666' 21 | git config --global user.email '1076988944@qq.com' 22 | yarn 23 | yarn test && yarn build 24 | - uses: JS-DevTools/npm-publish@v1 25 | with: 26 | token: ${{ secrets.NPM_AUTH_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | tests: 9 | name: Building package 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Caching node_modules 16 | uses: actions/cache@v3 17 | id: yarn-cache 18 | with: 19 | path: "**/node_modules" 20 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 21 | restore-keys: | 22 | ${{ runner.os }}-node- 23 | - name: Setup Node 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: '14' 27 | check-latest: true 28 | cache: 'yarn' 29 | 30 | - name: Install dependencies 🔧 31 | if: steps.yarn-cache.outputs.cache-hit != 'true' 32 | run: yarn install --frozen-lockfile 33 | 34 | - name: Test package 🔧 35 | run: yarn test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # builds 4 | build 5 | 6 | # misc 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | yarn.lock 11 | package-lock.json 12 | size-plugin.json 13 | stats.json 14 | stats.html 15 | 16 | # mac 17 | .DS_Store -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.19.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 liaoliao666 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |

7 | 8 | 9 | 10 |

11 | 12 |

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

13 | 14 |

15 | Latest build 16 | Latest published version 17 | Types included 18 | License 19 | Number of downloads 20 | GitHub Stars 21 |

22 |
23 | 24 | --- 25 | 26 | ## Motivation 27 | 28 | - 以类型安全的方式管理 `queryKey` 29 | - 让 `queryClient` 的操作更清楚地关联到哪个自定义 hook 30 | - 可以从任何自定义 ReactQuery hook 中提取的 TypeScript 类型 31 | - 中间件 32 | 33 | [English](./README.md) | 简体中文 34 | 35 | ## Table of Contents 36 | 37 | 38 | 39 | 40 | - [安装](#installation) 41 | - [例子](#examples) 42 | - 使用 43 | - [createQuery](#createquery) 44 | - [createInfiniteQuery](#createinfinitequery) 45 | - [createSuspenseQuery](#createsuspensequery) 46 | - [createSuspenseInfiniteQuery](#createsuspenseinfinitequery) 47 | - [createMutation](#createmutation) 48 | - [router](#router) 49 | - [中间件](#中间件) 50 | - [TypeScript](#typescript) 51 | - [类型推导](#类型推导) 52 | - [禁用查询](#禁用查询) 53 | - [常见问题](#常见问题) 54 | - [迁移](#迁移) 55 | - [Issues](#issues) 56 | - [🐛 Bugs](#-bugs) 57 | - [💡 Feature Requests](#-feature-requests) 58 | - [LICENSE](#license) 59 | 60 | 61 | 62 | ## Installation 63 | 64 | ```bash 65 | $ npm i react-query-kit 66 | # or 67 | $ yarn add react-query-kit 68 | ``` 69 | 70 | 如果您还在使用 React Query Kit v2? 请在此处查看 v2 文档:https://github.com/liaoliao666/react-query-kit/tree/v2#readme. 71 | 72 | # Examples 73 | 74 | - [Basic](https://codesandbox.io/s/example-react-query-kit-basic-1ny2j8) 75 | - [Optimistic Updates](https://codesandbox.io/s/example-react-query-kit-optimistic-updates-eefg0v) 76 | - [Next.js](https://codesandbox.io/s/example-react-query-kit-nextjs-uldl88) 77 | - [Load-More & Infinite Scroll](https://codesandbox.io/s/example-react-query-kit-load-more-infinite-scroll-vg494v) 78 | 79 | ## createQuery 80 | 81 | ### Usage 82 | 83 | ```tsx 84 | import { QueryClient, dehydrate } from '@tanstack/react-query' 85 | import { createQuery } from 'react-query-kit' 86 | 87 | type Data = { title: string; content: string } 88 | type Variables = { id: number } 89 | 90 | const usePost = createQuery({ 91 | queryKey: ['posts'], 92 | fetcher: (variables: Variables): Promise => { 93 | return fetch(`/posts/${variables.id}`).then(res => res.json()) 94 | }, 95 | // 你还可以通过中间件来定制这个 hook 的行为 96 | use: [myMiddleware] 97 | }) 98 | 99 | const variables = { id: 1 } 100 | 101 | // example 102 | export default function Page() { 103 | // queryKey 相等于 ['/posts', { id: 1 }] 104 | const { data } = usePost({ variables }) 105 | 106 | return ( 107 |
108 |
{data?.title}
109 |
{data?.content}
110 |
111 | ) 112 | } 113 | 114 | console.log(usePost.getKey()) // ['/posts'] 115 | console.log(usePost.getKey(variables)) // ['/posts', { id: 1 }] 116 | 117 | // nextjs 例子 118 | export async function getStaticProps() { 119 | const queryClient = new QueryClient() 120 | 121 | await queryClient.prefetchQuery(usePost.getFetchOptions(variables)) 122 | 123 | return { 124 | props: { 125 | dehydratedState: dehydrate(queryClient), 126 | }, 127 | } 128 | } 129 | 130 | // 在 react 组件外使用 131 | const data = await queryClient.fetchQuery( 132 | usePost.getFetchOptions(variables) 133 | ) 134 | 135 | // useQueries 例子 136 | const queries = useQueries({ 137 | queries: [ 138 | usePost.getOptions(variables), 139 | useUser.getOptions(), 140 | ], 141 | }) 142 | 143 | // getQueryData 144 | queryClient.getQueryData(usePost.getKey(variables)) // Data 145 | 146 | // setQueryData 147 | queryClient.setQueryData(usePost.getKey(variables), {...}) 148 | ``` 149 | 150 | ### 额外的 API 文档 151 | 152 | Options 153 | 154 | - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` 155 | - 必填 156 | - 用于请求数据的函数。 第二个参数是“queryFn”的“QueryFunctionContext” 157 | - `variables?: TVariables` 158 | - 可选 159 | - `variables` 将是 fetcher 的第一个参数和 `queryKey` 数组的最后一个元素 160 | - `use: Middleware[]` 161 | - 可选 162 | - 中间件函数数组 [(详情)](#中间件) 163 | 164 | Expose Methods 165 | 166 | - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` 167 | - `getKey: (variables: TVariables) => QueryKey` 168 | - `getOptions: (variables: TVariables) => UseInfiniteQueryOptions` 169 | - `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn })` 170 | 171 | ## createInfiniteQuery 172 | 173 | ### Usage 174 | 175 | ```tsx 176 | import { QueryClient, dehydrate } from '@tanstack/react-query' 177 | import { createInfiniteQuery } from 'react-query-kit' 178 | 179 | type Data = { projects: { id: string; name: string }[]; nextCursor: number } 180 | type Variables = { active: boolean } 181 | 182 | const useProjects = createInfiniteQuery({ 183 | queryKey: ['projects'], 184 | fetcher: (variables: Variables, { pageParam }): Promise => { 185 | return fetch( 186 | `/projects?cursor=${pageParam}?active=${variables.active}` 187 | ).then(res => res.json()) 188 | }, 189 | getNextPageParam: (lastPage, pages) => lastPage.nextCursor, 190 | initialPageParam: 0, 191 | }) 192 | 193 | const variables = { active: true } 194 | 195 | // example 196 | export default function Page() { 197 | // queryKey equals to ['projects', { active: true }] 198 | const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = 199 | useProjects({ variables }) 200 | 201 | return ( 202 |
203 | {data.pages.map((group, i) => ( 204 | 205 | {group.projects.map(project => ( 206 |

{project.name}

207 | ))} 208 |
209 | ))} 210 |
211 | 221 |
222 |
{isFetching && !isFetchingNextPage ? 'Fetching...' : null}
223 |
224 | ) 225 | } 226 | 227 | // nextjs example 228 | export async function getStaticProps() { 229 | const queryClient = new QueryClient() 230 | 231 | await queryClient.prefetchInfiniteQuery( 232 | useProjects.getFetchOptions(variables) 233 | ) 234 | 235 | return { 236 | props: { 237 | dehydratedState: dehydrate(queryClient), 238 | }, 239 | } 240 | } 241 | 242 | // 在 react 组件外使用 243 | const data = await queryClient.fetchInfiniteQuery( 244 | useProjects.getFetchOptions(variables) 245 | ) 246 | ``` 247 | 248 | ### 额外的 API 文档 249 | 250 | Options 251 | 252 | - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` 253 | - 必填 254 | - 查询将用于请求数据的函数。 第二个参数是“queryFn”的“QueryFunctionContext” 255 | - `variables?: TVariables` 256 | - 可选 257 | - `variables` 将是 fetcher 的第一个参数和 `queryKey` 数组的最后一个元素 258 | - `use: Middleware[]` 259 | - 可选 260 | - 中间件函数数组 [(详情)](#中间件) 261 | 262 | Expose Methods 263 | 264 | - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` 265 | - `getKey: (variables: TVariables) => QueryKey` 266 | - `getOptions: (variables: TVariables) => UseInfiniteQueryOptions` 267 | - `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn, getNextPageParam, getPreviousPageParam, initialPageParam })` 268 | 269 | ## createSuspenseQuery 270 | 271 | 这与在查询配置中将 suspense 选项设置为 true 具有相同的效果,但在 TypeScript 的体验更好,因为 data 是有定义的(因为错误和加载状态由 Suspense 和 ErrorBoundaries 处理)。 272 | 273 | ```ts 274 | import { createSuspenseQuery } from 'react-query-kit' 275 | 276 | createSuspenseQuery({ 277 | ...options, 278 | }) 279 | 280 | // 相当于 281 | createQuery({ 282 | ...options, 283 | enabled: true, 284 | suspense: true, 285 | throwOnError: true, 286 | }) 287 | ``` 288 | 289 | ## createSuspenseInfiniteQuery 290 | 291 | ```ts 292 | import { createSuspenseInfiniteQuery } from 'react-query-kit' 293 | 294 | createSuspenseInfiniteQuery({ 295 | ...options, 296 | }) 297 | 298 | // 相当于 299 | createInfiniteQuery({ 300 | ...options, 301 | enabled: true, 302 | suspense: true, 303 | throwOnError: true, 304 | }) 305 | ``` 306 | 307 | ## createMutation 308 | 309 | ### Usage 310 | 311 | ```tsx 312 | import { createMutation } from 'react-query-kit' 313 | 314 | const useAddTodo = createMutation( 315 | async (variables: { title: string; content: string }) => 316 | fetch('/post', { 317 | method: 'POST', 318 | body: JSON.stringify(variables), 319 | }).then(res => res.json()), 320 | { 321 | onSuccess(data, variables, context) { 322 | // do somethings 323 | }, 324 | } 325 | ) 326 | 327 | function App() { 328 | const mutation = useAddTodo({ 329 | onSettled: (data, error, variables, context) => { 330 | // Error or success... doesn't matter! 331 | }, 332 | }) 333 | 334 | return ( 335 |
336 | {mutation.isPending ? ( 337 | 'Adding todo...' 338 | ) : ( 339 | <> 340 | {mutation.isError ? ( 341 |
An error occurred: {mutation.error.message}
342 | ) : null} 343 | 344 | {mutation.isSuccess ?
Todo added!
: null} 345 | 346 | 353 | 354 | )} 355 |
356 | ) 357 | } 358 | 359 | // usage outside of react component 360 | useAddTodo.mutationFn({ title: 'Do Laundry', content: 'content...' }) 361 | ``` 362 | 363 | ### 额外的 API 文档 364 | 365 | Options 366 | 367 | - `use: Middleware[]` 368 | - 可选 369 | - 中间件函数数组 [(详情)](#中间件) 370 | 371 | Expose Methods 372 | 373 | - `getKey: () => MutationKey` 374 | - `getOptions: () => UseMutationOptions` 375 | - `mutationFn: MutationFunction` 376 | 377 | ## router 378 | 379 | `router` 允许您创建整个 API 的形状 380 | 381 | ### Usage 382 | 383 | ```tsx 384 | import { router } from 'react-query-kit' 385 | 386 | const post = router(`post`, { 387 | byId: router.query({ 388 | fetcher: (variables: { id: number }) => 389 | fetch(`/posts/${variables.id}`).then(res => res.json()), 390 | use: [myMiddleware], 391 | }), 392 | 393 | list: router.infiniteQuery({ 394 | fetcher: (_variables, { pageParam }) => 395 | fetch(`/posts/?cursor=${pageParam}`).then(res => res.json()), 396 | getNextPageParam: lastPage => lastPage.nextCursor, 397 | initialPageParam: 0, 398 | }), 399 | 400 | add: router.mutation({ 401 | mutationFn: async (variables: { title: string; content: string }) => 402 | fetch('/posts', { 403 | method: 'POST', 404 | body: JSON.stringify(variables), 405 | }).then(res => res.json()), 406 | }), 407 | 408 | // nest router 409 | command: { 410 | report: router.mutation({ mutationFn }), 411 | 412 | promote: router.mutation({ mutationFn }), 413 | }, 414 | }) 415 | 416 | // get root key 417 | post.getKey() // ['post'] 418 | 419 | // hooks 420 | post.byId.useQuery({ variables: { id: 1 } }) 421 | post.byId.useSuspenseQuery({ variables: { id: 1 } }) 422 | post.list.useInfiniteQuery() 423 | post.list.useSuspenseInfiniteQuery() 424 | post.add.useMutation() 425 | post.command.report.useMutation() 426 | 427 | // expose methods 428 | post.byId.getKey({ id: 1 }) // ['post', 'byId', { id: 1 }] 429 | post.byId.getFetchOptions({ id: 1 }) 430 | post.byId.getOptions({ id: 1 }) 431 | post.byId.fetcher({ id: 1 }) 432 | post.add.getKey() // ['post', 'add'] 433 | post.add.getOptions() 434 | post.add.mutationFn({ title: 'title', content: 'content' }) 435 | 436 | // infer types 437 | type Data = inferData 438 | type FnData = inferFnData 439 | type Variables = inferVariables 440 | type Error = inferError 441 | ``` 442 | 443 | ### 合并路由 444 | 445 | ```ts 446 | import { router } from 'react-query-kit' 447 | 448 | const user = router(`user`, {}) 449 | const post = router(`post`, {}) 450 | 451 | const k = { 452 | user, 453 | post, 454 | } 455 | ``` 456 | 457 | ### API 文档 458 | 459 | `type Router = (key: string | unknown[], config: TConfig) => TRouter` 460 | 461 | Expose Methods 462 | 463 | - `query` 464 | 与 `createQuery` 类似,但无需选项 `queryKey` 465 | - `infiniteQuery` 466 | 与 `createInfiniteQuery` 类似,但无需选项 `queryKey` 467 | - `mutation` 468 | 与 `createMutation` 类似,但无需选项 `mutationKey` 469 | 470 | ## 中间件 471 | 472 | 此功能的灵感来自于 [SWR 的中间件功能](https://swr.vercel.app/docs/middleware)。 473 | 474 | 中间件接收 hook,可以在运行它之前和之后执行逻辑。如果有多个中间件,则每个中间件包装下一个中间件。列表中的最后一个中间件将接收原始的 hook。 475 | 476 | ### 使用 477 | 478 | ```ts 479 | import { QueryClient } from '@tanstack/react-query' 480 | import { Middleware, MutationHook, QueryHook, getKey } from 'react-query-kit' 481 | 482 | const logger: Middleware> = useQueryNext => { 483 | return options => { 484 | const log = useLogger() 485 | const fetcher = (variables, context) => { 486 | log(context.queryKey, variables) 487 | return options.fetcher(variables, context) 488 | } 489 | 490 | return useQueryNext({ 491 | ...options, 492 | fetcher, 493 | }) 494 | } 495 | } 496 | 497 | const useUser = createQuery({ 498 | use: [logger], 499 | }) 500 | 501 | // 全局中间件 502 | const queryMiddleware: Middleware = useQueryNext => { 503 | return options => { 504 | // 你还可以通过函数 getKey 获取 queryKey 505 | const fullKey = getKey(options.queryKey, options.variables) 506 | // ... 507 | return useQueryNext(options) 508 | } 509 | } 510 | const mutationMiddleware: Middleware = useMutationNext => { 511 | return options => { 512 | // ... 513 | return useMutationNext(options) 514 | } 515 | } 516 | 517 | const queryClient = new QueryClient({ 518 | defaultOptions: { 519 | queries: { 520 | use: [queryMiddleware], 521 | }, 522 | mutations: { 523 | use: [mutationMiddleware], 524 | }, 525 | }, 526 | }) 527 | ``` 528 | 529 | ### 扩展 530 | 531 | 中间件将从上级合并。例如: 532 | 533 | ```jsx 534 | const queryClient = new QueryClient({ 535 | defaultOptions: { 536 | queries: { 537 | use: [a], 538 | }, 539 | }, 540 | }) 541 | 542 | const useSomething = createQuery({ 543 | use: [b], 544 | }) 545 | 546 | useSomething({ use: [c] }) 547 | ``` 548 | 549 | 相当于: 550 | 551 | ```js 552 | createQuery({ use: [a, b, c] }) 553 | ``` 554 | 555 | ### 多个中间件 556 | 557 | 每个中间件包装下一个中间件,最后一个只包装 useQuery hook。例如: 558 | 559 | ```jsx 560 | createQuery({ use: [a, b, c] }) 561 | ``` 562 | 563 | 中间件执行的顺序是 `a → b → c`,如下所示: 564 | 565 | ```plaintext 566 | enter a 567 | enter b 568 | enter c 569 | useQuery() 570 | exit c 571 | exit b 572 | exit a 573 | ``` 574 | 575 | ### 多个 QueryClient 576 | 577 | 在 ReactQuery v5 中,`QueryClient` 将是 `useQuery` 和 `useMutation` 的第二个参数。 如果你在全局中有多个 `QueryClient`,你应该在中间件钩子中接收 `QueryClient` 578 | 579 | ```ts 580 | const useSomething = createQuery({ 581 | use: [ 582 | function myMiddleware(useQueryNext) { 583 | // 你应该接收 queryClient 作为第二个参数 584 | return (options, queryClient) => { 585 | const client = useQueryClient(queryClient) 586 | // ... 587 | return useQueryNext(options, queryClient) 588 | } 589 | }, 590 | ], 591 | }) 592 | 593 | // 如果你传入另一个 QueryClient 594 | useSomething({...}, anotherQueryClient) 595 | ``` 596 | 597 | ## TypeScript 598 | 599 | 默认情况下,ReactQueryKit 还会从 `fetcher` 推断 `data` 和 `variables` 的类型,因此您可以自动获得首选类型。 600 | 601 | ```ts 602 | type Data = { title: string; content: string } 603 | type Variables = { id: number } 604 | 605 | const usePost = createQuery({ 606 | queryKey: ['posts'], 607 | fetcher: (variables: Variables): Promise => { 608 | return fetch(`/posts/${variables}`).then(res => res.json()) 609 | }, 610 | }) 611 | 612 | // `data` 将被推断为 `Data | undefined`. 613 | // `variables` 将被推断为 `Variables`. 614 | const { data } = usePost({ variables: { id: 1 } }) 615 | ``` 616 | 617 | 您还可以显式指定 `fetcher` 参数和返回的类型。 618 | 619 | ```ts 620 | type Data = { title: string; content: string } 621 | type Variables = { id: number } 622 | 623 | const usePost = createQuery({ 624 | queryKey: ['posts'], 625 | fetcher: variables => { 626 | return fetch(`/posts/${variables}`).then(res => res.json()) 627 | }, 628 | }) 629 | 630 | // `data` 将被推断为 `Data | undefined`. 631 | // `error` 将被推断为 `Error | null` 632 | // `variables` 将被推断为 `Variables`. 633 | const { data, error } = usePost({ variables: { id: 1 } }) 634 | ``` 635 | 636 | ## 类型推导 637 | 638 | 您可以使用 `inferData` 或 `inferVariables` 提取任何自定义 hook 的 TypeScript 类型 639 | 640 | ```ts 641 | import { inferData, inferFnData, inferError, inferVariables, inferOptions } from 'react-query-kit' 642 | 643 | const useProjects = createInfiniteQuery(...) 644 | 645 | inferData // InfiniteData 646 | inferFnData // Data 647 | inferVariables // Variables 648 | inferError // Error 649 | inferOptions // InfiniteQueryHookOptions<...> 650 | ``` 651 | 652 | ## 禁用查询 653 | 654 | 要禁用查询,您可以将 `skipToken` 作为选项 `variables` 传递给您的自定义查询。这将阻止查询被执行。 655 | 656 | ```ts 657 | import { skipToken } from '@tanstack/react-query' 658 | 659 | const [name, setName] = useState() 660 | const result = usePost({ 661 | variables: id ? { id: id } : skipToken, 662 | }) 663 | 664 | // 以及用于 useQueries 的示例 665 | const queries = useQueries({ 666 | queries: [usePost.getOptions(id ? { id: id } : skipToken)], 667 | }) 668 | ``` 669 | 670 | ## 常见问题 671 | 672 | ### `getFetchOptions` 和 `getOptions` 有什么不同 673 | 674 | `getFetchOptions` 只会返回必要的选项,而像 `staleTime` 和 `retry` 等选项会被忽略 675 | 676 | ### `fetcher` 和 `queryFn` 有什么不同 677 | 678 | ReactQueryKit 会自动将 `fetcher` 转换为 `queryFn`,例如 679 | 680 | ```ts 681 | const useTest = createQuery({ 682 | queryKey: ['test'], 683 | fetcher: (variables, context) => { 684 | // ... 685 | }, 686 | }) 687 | 688 | // => useTest.getOptions(variables): 689 | // { 690 | // queryKey: ['test', variables], 691 | // queryFn: (context) => fetcher(variables, context) 692 | // } 693 | ``` 694 | 695 | ## 迁移 696 | 697 | 从 ReactQueryKit 2 升级 → ReactQueryKit 3 698 | 699 | ```diff 700 | createQuery({ 701 | - primaryKey: 'posts', 702 | - queryFn: ({ queryKey: [_primaryKey, variables] }) => {}, 703 | + queryKey: ['posts'], 704 | + fetcher: variables => {}, 705 | }) 706 | ``` 707 | 708 | 您可以从 ReactQueryKit 3 中受益 709 | 710 | - 支持传入数组 `queryKey` 711 | - 支持推断 fetcher 的类型,您可以自动享受首选的类型。 712 | - 支持创建整个 API 的形状 713 | 714 | ## Issues 715 | 716 | _Looking to contribute? Look for the [Good First Issue][good-first-issue] 717 | label._ 718 | 719 | ### 🐛 Bugs 720 | 721 | 请针对错误、缺少文档或意外行为提出问题。 722 | 723 | [**See Bugs**][bugs] 724 | 725 | ### 💡 Feature Requests 726 | 727 | 请提交问题以建议新功能。 通过添加对功能请求进行投票 728 | 一个 👍。 这有助于维护人员优先处理要处理的内容。 729 | 730 | [**See Feature Requests**][requests] 731 | 732 | ## LICENSE 733 | 734 | MIT 735 | 736 | 737 | [npm]: https://www.npmjs.com 738 | [node]: https://nodejs.org 739 | [bugs]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug 740 | [requests]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement 741 | [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 742 | 743 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |

7 | 8 | 9 | 10 |

11 | 12 |

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

13 | 14 |

15 | Latest build 16 | Latest published version 17 | Types included 18 | License 19 | Number of downloads 20 | GitHub Stars 21 |

22 |
23 | 24 | --- 25 | 26 | ## What could you benefit from it 27 | 28 | - Manage `queryKey` in a type-safe way 29 | - Make `queryClient`'s operations clearly associated with custom ReactQuery hooks 30 | - You can extract the TypeScript type of any custom ReactQuery hooks 31 | - Middleware 32 | 33 | English | [简体中文](./README-zh_CN.md) 34 | 35 | ## Table of Contents 36 | 37 | 38 | 39 | 40 | - [Installation](#installation) 41 | - [Examples](#examples) 42 | - Usage 43 | - [createQuery](#createquery) 44 | - [createInfiniteQuery](#createinfinitequery) 45 | - [createSuspenseQuery](#createsuspensequery) 46 | - [createSuspenseInfiniteQuery](#createsuspenseinfinitequery) 47 | - [createMutation](#createmutation) 48 | - [router](#router) 49 | - [Middleware](#middleware) 50 | - [TypeScript](#typescript) 51 | - [Type inference](#type-inference) 52 | - [Disabling Queries](#disabling-queries) 53 | - [FAQ](#faq) 54 | - [Migration](#migration) 55 | - [Issues](#issues) 56 | - [🐛 Bugs](#-bugs) 57 | - [💡 Feature Requests](#-feature-requests) 58 | - [LICENSE](#license) 59 | 60 | 61 | 62 | ## Installation 63 | 64 | This module is distributed via [npm][npm] which is bundled with [node][node] and 65 | should be installed as one of your project's `dependencies`: 66 | 67 | ```bash 68 | $ npm i react-query-kit 69 | # or 70 | $ yarn add react-query-kit 71 | ``` 72 | 73 | If you still on React Query Kit v2? Check out the v2 docs here: https://github.com/liaoliao666/react-query-kit/tree/v2#readme. 74 | 75 | # Examples 76 | 77 | - [Basic](https://codesandbox.io/s/example-react-query-kit-basic-1ny2j8) 78 | - [Optimistic Updates](https://codesandbox.io/s/example-react-query-kit-optimistic-updates-eefg0v) 79 | - [Next.js](https://codesandbox.io/s/example-react-query-kit-nextjs-uldl88) 80 | - [Load-More & Infinite Scroll](https://codesandbox.io/s/example-react-query-kit-load-more-infinite-scroll-vg494v) 81 | 82 | ## createQuery 83 | 84 | ### Usage 85 | 86 | ```tsx 87 | import { QueryClient, dehydrate } from '@tanstack/react-query' 88 | import { createQuery } from 'react-query-kit' 89 | 90 | type Data = { title: string; content: string } 91 | type Variables = { id: number } 92 | 93 | const usePost = createQuery({ 94 | queryKey: ['posts'], 95 | fetcher: (variables: Variables): Promise => { 96 | return fetch(`/posts/${variables.id}`).then(res => res.json()) 97 | }, 98 | // u can also pass middleware to cutomize this hook's behavior 99 | use: [myMiddleware] 100 | }) 101 | 102 | 103 | const variables = { id: 1 } 104 | 105 | // example 106 | export default function Page() { 107 | // queryKey will be `['posts', { id: 1 }]` if u passed variables 108 | const { data } = usePost({ variables }) 109 | 110 | return ( 111 |
112 |
{data?.title}
113 |
{data?.content}
114 |
115 | ) 116 | } 117 | 118 | console.log(usePost.getKey()) // ['posts'] 119 | console.log(usePost.getKey(variables)) // ['posts', { id: 1 }] 120 | 121 | // nextjs example 122 | export async function getStaticProps() { 123 | const queryClient = new QueryClient() 124 | 125 | await queryClient.prefetchQuery(usePost.getFetchOptions(variables)) 126 | 127 | return { 128 | props: { 129 | dehydratedState: dehydrate(queryClient), 130 | }, 131 | } 132 | } 133 | 134 | // usage outside of react component 135 | const data = await queryClient.fetchQuery(usePost.getFetchOptions(variables)) 136 | 137 | // useQueries example 138 | const queries = useQueries({ 139 | queries: [ 140 | usePost.getOptions(variables), 141 | useUser.getOptions(), 142 | ], 143 | }) 144 | 145 | // getQueryData 146 | queryClient.getQueryData(usePost.getKey(variables)) // Data 147 | 148 | // setQueryData 149 | queryClient.setQueryData(usePost.getKey(variables), {...}) 150 | ``` 151 | 152 | ### Additional API Reference 153 | 154 | Options 155 | 156 | - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` 157 | - Required 158 | - The function that the query will use to request data. And The second param is the `QueryFunctionContext` of `queryFn`. 159 | - `variables?: TVariables` 160 | - Optional 161 | - `variables` will be the frist param of fetcher and the last element of the `queryKey` array 162 | - `use: Middleware[]` 163 | - Optional 164 | - array of middleware functions [(details)](#middleware) 165 | 166 | Expose Methods 167 | 168 | - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` 169 | - `getKey: (variables: TVariables) => QueryKey` 170 | - `getOptions: (variables: TVariables) => UseQueryOptions` 171 | - `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn })` 172 | 173 | ## createInfiniteQuery 174 | 175 | ### Usage 176 | 177 | ```tsx 178 | import { QueryClient, dehydrate } from '@tanstack/react-query' 179 | import { createInfiniteQuery } from 'react-query-kit' 180 | 181 | type Data = { projects: { id: string; name: string }[]; nextCursor: number } 182 | type Variables = { active: boolean } 183 | 184 | const useProjects = createInfiniteQuery({ 185 | queryKey: ['projects'], 186 | fetcher: (variables: Variables, { pageParam }): Promise => { 187 | return fetch( 188 | `/projects?cursor=${pageParam}?active=${variables.active}` 189 | ).then(res => res.json()) 190 | }, 191 | getNextPageParam: (lastPage, pages) => lastPage.nextCursor, 192 | initialPageParam: 0, 193 | }) 194 | 195 | const variables = { active: true } 196 | 197 | // example 198 | export default function Page() { 199 | // queryKey equals to ['projects', { active: true }] 200 | const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = 201 | useProjects({ variables }) 202 | 203 | return ( 204 |
205 | {data.pages.map((group, i) => ( 206 | 207 | {group.projects.map(project => ( 208 |

{project.name}

209 | ))} 210 |
211 | ))} 212 |
213 | 223 |
224 |
{isFetching && !isFetchingNextPage ? 'Fetching...' : null}
225 |
226 | ) 227 | } 228 | 229 | // nextjs example 230 | export async function getStaticProps() { 231 | const queryClient = new QueryClient() 232 | 233 | await queryClient.prefetchInfiniteQuery( 234 | useProjects.getFetchOptions(variables) 235 | ) 236 | 237 | return { 238 | props: { 239 | dehydratedState: dehydrate(queryClient), 240 | }, 241 | } 242 | } 243 | 244 | // usage outside of react component 245 | const data = await queryClient.fetchInfiniteQuery( 246 | useProjects.getFetchOptions(variables) 247 | ) 248 | ``` 249 | 250 | ### Additional API Reference 251 | 252 | Options 253 | 254 | - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` 255 | - Required 256 | - The function that the query will use to request data. And The second param is the `QueryFunctionContext` of `queryFn`. 257 | - `variables?: TVariables` 258 | - Optional 259 | - `variables` will be the frist param of fetcher and the last element of the `queryKey` array 260 | - `use: Middleware[]` 261 | - Optional 262 | - array of middleware functions [(details)](#middleware) 263 | 264 | Expose Methods 265 | 266 | - `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise` 267 | - `getKey: (variables: TVariables) => QueryKey` 268 | - `getOptions: (variables: TVariables) => UseInfiniteQueryOptions` 269 | - `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn, getNextPageParam, getPreviousPageParam, initialPageParam })` 270 | 271 | ## createSuspenseQuery 272 | 273 | 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). 274 | 275 | ```ts 276 | import { createSuspenseQuery } from 'react-query-kit' 277 | 278 | createSuspenseQuery({ 279 | ...options, 280 | }) 281 | 282 | // equals to 283 | createQuery({ 284 | ...options, 285 | enabled: true, 286 | suspense: true, 287 | throwOnError: true, 288 | }) 289 | ``` 290 | 291 | ## createSuspenseInfiniteQuery 292 | 293 | ```ts 294 | import { createSuspenseInfiniteQuery } from 'react-query-kit' 295 | 296 | createSuspenseInfiniteQuery({ 297 | ...options, 298 | }) 299 | 300 | // equals to 301 | createInfiniteQuery({ 302 | ...options, 303 | enabled: true, 304 | suspense: true, 305 | throwOnError: true, 306 | }) 307 | ``` 308 | 309 | ## createMutation 310 | 311 | ### Usage 312 | 313 | ```tsx 314 | import { createMutation } from 'react-query-kit' 315 | 316 | const useAddTodo = createMutation({ 317 | mutationFn: async (variables: { title: string; content: string }) => 318 | fetch('/post', { 319 | method: 'POST', 320 | body: JSON.stringify(variables), 321 | }).then(res => res.json()), 322 | onSuccess(data, variables, context) { 323 | // do somethings 324 | }, 325 | }) 326 | 327 | function App() { 328 | const mutation = useAddTodo({ 329 | onSettled: (data, error, variables, context) => { 330 | // Error or success... doesn't matter! 331 | }, 332 | }) 333 | 334 | return ( 335 |
336 | {mutation.isPending ? ( 337 | 'Adding todo...' 338 | ) : ( 339 | <> 340 | {mutation.isError ? ( 341 |
An error occurred: {mutation.error.message}
342 | ) : null} 343 | 344 | {mutation.isSuccess ?
Todo added!
: null} 345 | 346 | 353 | 354 | )} 355 |
356 | ) 357 | } 358 | 359 | // usage outside of react component 360 | useAddTodo.mutationFn({ title: 'Do Laundry', content: 'content...' }) 361 | ``` 362 | 363 | ### Additional API Reference 364 | 365 | Options 366 | 367 | - `use: Middleware[]` 368 | - Optional 369 | - array of middleware functions [(details)](#middleware) 370 | 371 | Expose Methods 372 | 373 | - `getKey: () => MutationKey` 374 | - `getOptions: () => UseMutationOptions` 375 | - `mutationFn: MutationFunction` 376 | 377 | ## router 378 | 379 | `router` which allow you to create a shape of your entire API 380 | 381 | ### Usage 382 | 383 | ```tsx 384 | import { router } from 'react-query-kit' 385 | 386 | const post = router(`post`, { 387 | byId: router.query({ 388 | fetcher: (variables: { id: number }) => 389 | fetch(`/posts/${variables.id}`).then(res => res.json()), 390 | use: [myMiddleware], 391 | }), 392 | 393 | list: router.infiniteQuery({ 394 | fetcher: (_variables, { pageParam }) => 395 | fetch(`/posts/?cursor=${pageParam}`).then(res => res.json()), 396 | getNextPageParam: lastPage => lastPage.nextCursor, 397 | initialPageParam: 0, 398 | }), 399 | 400 | add: router.mutation({ 401 | mutationFn: async (variables: { title: string; content: string }) => 402 | fetch('/posts', { 403 | method: 'POST', 404 | body: JSON.stringify(variables), 405 | }).then(res => res.json()), 406 | }), 407 | 408 | // nest router 409 | command: { 410 | report: router.mutation({ mutationFn }), 411 | 412 | promote: router.mutation({ mutationFn }), 413 | }, 414 | }) 415 | 416 | // get root key 417 | post.getKey() // ['post'] 418 | 419 | // hooks 420 | post.byId.useQuery({ variables: { id: 1 } }) 421 | post.byId.useSuspenseQuery({ variables: { id: 1 } }) 422 | post.list.useInfiniteQuery() 423 | post.list.useSuspenseInfiniteQuery() 424 | post.add.useMutation() 425 | post.command.report.useMutation() 426 | 427 | // expose methods 428 | post.byId.getKey({ id: 1 }) // ['post', 'byId', { id: 1 }] 429 | post.byId.getFetchOptions({ id: 1 }) 430 | post.byId.getOptions({ id: 1 }) 431 | post.byId.fetcher({ id: 1 }) 432 | post.add.getKey() // ['post', 'add'] 433 | post.add.getOptions() 434 | post.add.mutationFn({ title: 'title', content: 'content' }) 435 | 436 | // infer types 437 | type Data = inferData 438 | type FnData = inferFnData 439 | type Variables = inferVariables 440 | type Error = inferError 441 | ``` 442 | 443 | ### Merging Routers 444 | 445 | ```ts 446 | import { router } from 'react-query-kit' 447 | 448 | const user = router(`user`, {}) 449 | const post = router(`post`, {}) 450 | 451 | const k = { 452 | user, 453 | post, 454 | } 455 | ``` 456 | 457 | ### API Reference 458 | 459 | `type Router = (key: string | unknown[], config: TConfig) => TRouter` 460 | 461 | Expose Methods 462 | 463 | - `query` 464 | Similar to `createQuery` but without option `queryKey` 465 | - `infiniteQuery` 466 | Similar to `createInfiniteQuery` but without option `queryKey` 467 | - `mutation` 468 | Similar to `createMutation` but without option `mutationKey` 469 | 470 | ## Middleware 471 | 472 | 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. 473 | 474 | 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. 475 | 476 | ### Usage 477 | 478 | ```ts 479 | import { QueryClient } from '@tanstack/react-query' 480 | import { Middleware, MutationHook, QueryHook, getKey } from 'react-query-kit' 481 | 482 | const logger: Middleware> = useQueryNext => { 483 | return options => { 484 | const log = useLogger() 485 | const fetcher = (variables, context) => { 486 | log(context.queryKey, variables) 487 | return options.fetcher(variables, context) 488 | } 489 | 490 | return useQueryNext({ 491 | ...options, 492 | fetcher, 493 | }) 494 | } 495 | } 496 | 497 | const useUser = createQuery({ 498 | use: [logger], 499 | }) 500 | 501 | // global middlewares 502 | const queryMiddleware: Middleware = useQueryNext => { 503 | return options => { 504 | // u can also get queryKey via function getKey 505 | const fullKey = getKey(options.queryKey, options.variables) 506 | // ... 507 | return useQueryNext(options) 508 | } 509 | } 510 | const mutationMiddleware: Middleware = useMutationNext => { 511 | return options => { 512 | // ... 513 | return useMutationNext(options) 514 | } 515 | } 516 | 517 | const queryClient = new QueryClient({ 518 | defaultOptions: { 519 | queries: { 520 | use: [queryMiddleware], 521 | }, 522 | mutations: { 523 | use: [mutationMiddleware], 524 | }, 525 | }, 526 | }) 527 | ``` 528 | 529 | ### Extend 530 | 531 | Middleware will be merged from superior. For example: 532 | 533 | ```jsx 534 | const queryClient = new QueryClient({ 535 | defaultOptions: { 536 | queries: { 537 | use: [a], 538 | }, 539 | }, 540 | }) 541 | 542 | const useSomething = createQuery({ 543 | use: [b], 544 | }) 545 | 546 | useSomething({ use: [c] }) 547 | ``` 548 | 549 | is equivalent to: 550 | 551 | ```js 552 | createQuery({ use: [a, b, c] }) 553 | ``` 554 | 555 | ### Multiple Middleware 556 | 557 | Each middleware wraps the next middleware, and the last one just wraps the useQuery. For example: 558 | 559 | ```jsx 560 | createQuery({ use: [a, b, c] }) 561 | ``` 562 | 563 | The order of middleware executions will be a → b → c, as shown below: 564 | 565 | ```plaintext 566 | enter a 567 | enter b 568 | enter c 569 | useQuery() 570 | exit c 571 | exit b 572 | exit a 573 | ``` 574 | 575 | ### Multiple QueryClient 576 | 577 | 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. 578 | 579 | ```ts 580 | const useSomething = createQuery({ 581 | use: [ 582 | function myMiddleware(useQueryNext) { 583 | // u should receive queryClient as the second argument here 584 | return (options, queryClient) => { 585 | const client = useQueryClient(queryClient) 586 | // ... 587 | return useQueryNext(options, queryClient) 588 | } 589 | }, 590 | ], 591 | }) 592 | 593 | // if u need to pass an another QueryClient 594 | useSomething({...}, anotherQueryClient) 595 | ``` 596 | 597 | ## TypeScript 598 | 599 | By default, ReactQueryKit will also infer the types of `data` and `variables` from `fetcher`, so you can have the preferred types automatically. 600 | 601 | ```ts 602 | type Data = { title: string; content: string } 603 | type Variables = { id: number } 604 | 605 | const usePost = createQuery({ 606 | queryKey: ['posts'], 607 | fetcher: (variables: Variables): Promise => { 608 | return fetch(`/posts/${variables}`).then(res => res.json()) 609 | }, 610 | }) 611 | 612 | // `data` will be inferred as `Data | undefined`. 613 | // `variables` will be inferred as `Variables`. 614 | const { data } = usePost({ variables: { id: 1 } }) 615 | ``` 616 | 617 | You can also explicitly specify the types for `fetcher`‘s `variables` and `data`. 618 | 619 | ```ts 620 | type Data = { title: string; content: string } 621 | type Variables = { id: number } 622 | 623 | const usePost = createQuery({ 624 | queryKey: ['posts'], 625 | fetcher: variables => { 626 | return fetch(`/posts/${variables}`).then(res => res.json()) 627 | }, 628 | }) 629 | 630 | // `data` will be inferred as `Data | undefined`. 631 | // `error` will be inferred as `Error | null` 632 | // `variables` will be inferred as `Variables`. 633 | const { data, error } = usePost({ variables: { id: 1 } }) 634 | ``` 635 | 636 | ## Type inference 637 | 638 | You can extract the TypeScript type of any custom hook with `inferData` or `inferVariables` 639 | 640 | ```ts 641 | import { inferData, inferFnData, inferError, inferVariables, inferOptions } from 'react-query-kit' 642 | 643 | const useProjects = createInfiniteQuery(...) 644 | 645 | inferData // InfiniteData 646 | inferFnData // Data 647 | inferVariables // Variables 648 | inferError // Error 649 | inferOptions // InfiniteQueryHookOptions<...> 650 | ``` 651 | 652 | ## Disabling Queries 653 | 654 | To disable queries, you can pass `skipToken` as the option `variables` to your custom query. This will prevent the query from being executed. 655 | 656 | ```ts 657 | import { skipToken } from '@tanstack/react-query' 658 | 659 | const [name, setName] = useState() 660 | const result = usePost({ 661 | variables: id ? { id: id } : skipToken, 662 | }) 663 | 664 | // and for useQueries example 665 | const queries = useQueries({ 666 | queries: [usePost.getOptions(id ? { id: id } : skipToken)], 667 | }) 668 | ``` 669 | 670 | ## FAQ 671 | 672 | ### What is the difference between `getFetchOptions` and `getOptions`? 673 | 674 | `getFetchOptions` would only return necessary options, while options like `staleTime` and `retry` would be omited 675 | 676 | ### What is the difference between `fetcher` and `queryFn`? 677 | 678 | ReactQueryKit would automatically converts fetcher to queryFn, as shown below: 679 | 680 | ```ts 681 | const useTest = createQuery({ 682 | queryKey: ['test'], 683 | fetcher: (variables, context) => { 684 | // ... 685 | }, 686 | }) 687 | 688 | // => useTest.getOptions(variables): 689 | // { 690 | // queryKey: ['test', variables], 691 | // queryFn: (context) => fetcher(variables, context) 692 | // } 693 | ``` 694 | 695 | ## Migration 696 | 697 | Upgrading from ReactQueryKit 2 → ReactQueryKit 3 698 | 699 | ```diff 700 | createQuery({ 701 | - primaryKey: 'posts', 702 | - queryFn: ({ queryKey: [_primaryKey, variables] }) => {}, 703 | + queryKey: ['posts'], 704 | + fetcher: variables => {}, 705 | }) 706 | ``` 707 | 708 | What you benefit from ReactQueryKit 3 709 | 710 | - Support hierarchical key 711 | - Support infer the types of fetcher, you can enjoy the preferred types automatically. 712 | - Support to create a shape of your entire API 713 | 714 | ## Issues 715 | 716 | _Looking to contribute? Look for the [Good First Issue][good-first-issue] 717 | label._ 718 | 719 | ### 🐛 Bugs 720 | 721 | Please file an issue for bugs, missing documentation, or unexpected behavior. 722 | 723 | [**See Bugs**][bugs] 724 | 725 | ### 💡 Feature Requests 726 | 727 | Please file an issue to suggest new features. Vote on feature requests by adding 728 | a 👍. This helps maintainers prioritize what to work on. 729 | 730 | [**See Feature Requests**][requests] 731 | 732 | ## LICENSE 733 | 734 | MIT 735 | 736 | 737 | [npm]: https://www.npmjs.com 738 | [node]: https://nodejs.org 739 | [bugs]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug 740 | [requests]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement 741 | [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 742 | 743 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | loose: true, 7 | modules: false, 8 | exclude: [ 9 | '@babel/plugin-transform-regenerator', 10 | '@babel/plugin-transform-parameters', 11 | ], 12 | }, 13 | ], 14 | '@babel/preset-typescript', 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('jest').Config} */ 2 | const config = { 3 | transform: { 4 | '\\.[jt]sx?$': 'ts-jest', 5 | }, 6 | testEnvironment: 'jsdom', 7 | } 8 | 9 | module.exports = config 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-query-kit", 3 | "version": "3.3.1", 4 | "description": "🕊️ A toolkit for ReactQuery that make ReactQuery hooks more reusable and typesafe", 5 | "author": "liaoliao666", 6 | "repository": "liaoliao666/react-query-kit", 7 | "homepage": "https://github.com/liaoliao666/react-query-kit#readme", 8 | "types": "build/lib/index.d.ts", 9 | "main": "build/lib/index.js", 10 | "module": "build/lib/index.esm.js", 11 | "exports": { 12 | ".": { 13 | "types": "./build/lib/index.d.ts", 14 | "import": "./build/lib/index.mjs", 15 | "default": "./build/lib/index.js" 16 | }, 17 | "./package.json": "./package.json" 18 | }, 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@babel/core": "^7.18.10", 22 | "@babel/preset-env": "^7.18.10", 23 | "@babel/preset-typescript": "^7.18.6", 24 | "@commitlint/cli": "^17.0.3", 25 | "@commitlint/config-conventional": "^17.0.3", 26 | "@rollup/plugin-babel": "^5.3.1", 27 | "@rollup/plugin-commonjs": "^22.0.2", 28 | "@rollup/plugin-node-resolve": "^13.2.1", 29 | "@rollup/plugin-replace": "^4.0.0", 30 | "@tanstack/react-query": "^5.59.16", 31 | "@testing-library/jest-dom": "^5.16.5", 32 | "@testing-library/react": "^14.0.0", 33 | "@trivago/prettier-plugin-sort-imports": "^4.2.0", 34 | "@types/jest": "^28.1.6", 35 | "@typescript-eslint/eslint-plugin": "^5.32.0", 36 | "@typescript-eslint/parser": "^5.32.0", 37 | "eslint": "^8.21.0", 38 | "eslint-config-prettier": "^8.5.0", 39 | "eslint-plugin-import": "^2.26.0", 40 | "eslint-plugin-jest": "^26.8.0", 41 | "eslint-plugin-prettier": "^4.2.1", 42 | "eslint-plugin-react": "^7.30.1", 43 | "eslint-plugin-react-hooks": "^4.6.0", 44 | "husky": "^8.0.1", 45 | "jest": "^29.5.0", 46 | "jest-environment-jsdom": "^29.5.0", 47 | "prettier": "^2.7.1", 48 | "react": "^18.2.0", 49 | "react-dom": "^18.2.0", 50 | "replace": "^1.2.1", 51 | "rollup": "^2.77.2", 52 | "rollup-plugin-size": "^0.2.2", 53 | "rollup-plugin-terser": "^7.0.2", 54 | "rollup-plugin-visualizer": "^5.7.1", 55 | "ts-jest": "^29.1.0", 56 | "typescript": "^5.1.6" 57 | }, 58 | "peerDependencies": { 59 | "@tanstack/react-query": "^4 || ^5" 60 | }, 61 | "peerDependenciesMeta": { 62 | "@tanstack/react-query": { 63 | "optional": true 64 | } 65 | }, 66 | "sideEffects": false, 67 | "scripts": { 68 | "build": "rollup --config rollup.config.js && npm run typecheck", 69 | "typecheck": "tsc -b", 70 | "stats": "open ./build/stats-html.html", 71 | "eslint": "eslint --fix '*.{js,json}' '{src,tests,benchmarks}/**/*.{ts,tsx}'", 72 | "test": "jest" 73 | }, 74 | "dependencies": {}, 75 | "files": [ 76 | "build/*", 77 | "src" 78 | ], 79 | "keywords": [ 80 | "react", 81 | "react-query" 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: false, 6 | singleQuote: true, 7 | trailingComma: 'es5', 8 | bracketSpacing: true, 9 | jsxBracketSameLine: false, 10 | arrowParens: 'avoid', 11 | endOfLine: 'auto', 12 | plugins: [require('@trivago/prettier-plugin-sort-imports')], 13 | importOrder: ['^[./]'], 14 | importOrderSeparation: true, 15 | importOrderSortSpecifiers: true, 16 | } 17 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel' 2 | import commonJS from '@rollup/plugin-commonjs' 3 | import { nodeResolve } from '@rollup/plugin-node-resolve' 4 | import replace from '@rollup/plugin-replace' 5 | import size from 'rollup-plugin-size' 6 | import { terser } from 'rollup-plugin-terser' 7 | import visualizer from 'rollup-plugin-visualizer' 8 | 9 | const replaceDevPlugin = type => 10 | replace({ 11 | 'process.env.NODE_ENV': `"${type}"`, 12 | delimiters: ['', ''], 13 | preventAssignment: true, 14 | }) 15 | const extensions = ['.ts', '.tsx'] 16 | const babelPlugin = babel({ 17 | babelHelpers: 'bundled', 18 | exclude: /node_modules/, 19 | extensions, 20 | }) 21 | 22 | export default function rollup() { 23 | const options = { 24 | input: 'src/index.ts', 25 | jsName: 'ReactQueryKit', 26 | external: ['@tanstack/react-query'], 27 | globals: { 28 | '@tanstack/react-query': 'ReactQuery', 29 | }, 30 | } 31 | 32 | return [ 33 | mjs(options), 34 | esm(options), 35 | cjs(options), 36 | umdDev(options), 37 | umdProd(options), 38 | ] 39 | } 40 | 41 | function mjs({ input, external }) { 42 | return { 43 | // ESM 44 | external, 45 | input, 46 | output: { 47 | format: 'esm', 48 | sourcemap: true, 49 | dir: `build/lib`, 50 | preserveModules: true, 51 | entryFileNames: '[name].mjs', 52 | }, 53 | plugins: [babelPlugin, commonJS(), nodeResolve({ extensions })], 54 | } 55 | } 56 | 57 | function esm({ input, external }) { 58 | return { 59 | // ESM 60 | external, 61 | input, 62 | output: { 63 | format: 'esm', 64 | dir: `build/lib`, 65 | sourcemap: true, 66 | preserveModules: true, 67 | entryFileNames: '[name].esm.js', 68 | }, 69 | plugins: [babelPlugin, commonJS(), nodeResolve({ extensions })], 70 | } 71 | } 72 | 73 | function cjs({ input, external }) { 74 | return { 75 | // CJS 76 | external, 77 | input, 78 | output: { 79 | format: 'cjs', 80 | sourcemap: true, 81 | dir: `build/lib`, 82 | preserveModules: true, 83 | exports: 'named', 84 | entryFileNames: '[name].js', 85 | }, 86 | plugins: [babelPlugin, commonJS(), nodeResolve({ extensions })], 87 | } 88 | } 89 | 90 | function umdDev({ input, external, globals, jsName }) { 91 | return { 92 | // UMD (Dev) 93 | external, 94 | input, 95 | output: { 96 | format: 'umd', 97 | sourcemap: true, 98 | file: `build/umd/index.development.js`, 99 | name: jsName, 100 | globals, 101 | }, 102 | plugins: [ 103 | babelPlugin, 104 | commonJS(), 105 | nodeResolve({ extensions }), 106 | replaceDevPlugin('development'), 107 | ], 108 | } 109 | } 110 | 111 | function umdProd({ input, external, globals, jsName }) { 112 | return { 113 | // UMD (Prod) 114 | external, 115 | input, 116 | output: { 117 | format: 'umd', 118 | sourcemap: true, 119 | file: `build/umd/index.production.js`, 120 | name: jsName, 121 | globals, 122 | }, 123 | plugins: [ 124 | babelPlugin, 125 | commonJS(), 126 | nodeResolve({ extensions }), 127 | replaceDevPlugin('production'), 128 | terser({ 129 | mangle: true, 130 | compress: true, 131 | }), 132 | size({}), 133 | visualizer({ 134 | filename: `build/stats-html.html`, 135 | gzipSize: true, 136 | }), 137 | visualizer({ 138 | filename: `build/stats.json`, 139 | json: true, 140 | gzipSize: true, 141 | }), 142 | ], 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/createBaseQuery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type QueryClient, 3 | type QueryFunctionContext, 4 | type UseBaseQueryOptions, 5 | type UseInfiniteQueryOptions, 6 | } from '@tanstack/react-query' 7 | 8 | import { ReactQuery, getKey as getFullKey, withMiddleware } from './utils' 9 | 10 | type QueryBaseHookOptions = Omit< 11 | UseBaseQueryOptions, 12 | 'queryKey' | 'queryFn' 13 | > & { 14 | fetcher?: any 15 | variables?: any 16 | } 17 | 18 | export const createBaseQuery = ( 19 | defaultOptions: any, 20 | useRQHook: (options: any, queryClient?: any) => any, 21 | overrideOptions?: Partial 22 | ): any => { 23 | if (process.env.NODE_ENV !== 'production') { 24 | // @ts-ignore 25 | if (defaultOptions.useDefaultOptions) { 26 | console.error( 27 | '[Bug] useDefaultOptions is not supported, please use middleware instead.' 28 | ) 29 | } 30 | 31 | // @ts-ignore 32 | if (defaultOptions.queryFn) { 33 | console.error( 34 | '[Bug] queryFn is not supported, please use fetcher instead.' 35 | ) 36 | } 37 | } 38 | 39 | const getQueryOptions = (fetcherFn: any, variables: any) => { 40 | return { 41 | queryFn: 42 | variables && variables === ReactQuery.skipToken 43 | ? ReactQuery.skipToken 44 | : (context: QueryFunctionContext) => fetcherFn(variables, context), 45 | queryKey: getFullKey(defaultOptions.queryKey, variables), 46 | } 47 | } 48 | 49 | const getKey = (variables?: any) => 50 | getFullKey(defaultOptions.queryKey, variables) 51 | 52 | const getOptions = (variables: any) => { 53 | return { 54 | ...defaultOptions, 55 | ...getQueryOptions(defaultOptions.fetcher, variables), 56 | } 57 | } 58 | 59 | const getFetchOptions = (variables: any) => { 60 | return { 61 | ...getQueryOptions(defaultOptions.fetcher, variables), 62 | queryKeyHashFn: defaultOptions.queryKeyHashFn, 63 | getPreviousPageParam: defaultOptions.getPreviousPageParam, 64 | getNextPageParam: defaultOptions.getNextPageParam, 65 | initialPageParam: defaultOptions.initialPageParam, 66 | } 67 | } 68 | 69 | const useBaseHook = ( 70 | options: QueryBaseHookOptions, 71 | queryClient?: QueryClient 72 | ) => { 73 | return useRQHook( 74 | { 75 | ...options, 76 | ...getQueryOptions(options.fetcher, options.variables), 77 | ...overrideOptions, 78 | }, 79 | queryClient 80 | ) 81 | } 82 | 83 | return Object.assign(withMiddleware(useBaseHook, defaultOptions, 'queries'), { 84 | fetcher: defaultOptions.fetcher, 85 | getKey, 86 | getOptions, 87 | getFetchOptions, 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /src/createInfiniteQuery.ts: -------------------------------------------------------------------------------- 1 | import { createBaseQuery } from './createBaseQuery' 2 | import type { 3 | CompatibleError, 4 | CreateInfiniteQueryOptions, 5 | InfiniteQueryHook, 6 | } from './types' 7 | import { ReactQuery } from './utils' 8 | 9 | export function createInfiniteQuery< 10 | TFnData, 11 | TVariables = void, 12 | TError = CompatibleError, 13 | TPageParam = number 14 | >( 15 | options: CreateInfiniteQueryOptions 16 | ): InfiniteQueryHook { 17 | return createBaseQuery(options, ReactQuery.useInfiniteQuery) 18 | } 19 | -------------------------------------------------------------------------------- /src/createMutation.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CompatibleError, 3 | CreateMutationOptions, 4 | MutationHook, 5 | } from './types' 6 | import { ReactQuery, withMiddleware } from './utils' 7 | 8 | export function createMutation< 9 | TData = unknown, 10 | TVariables = void, 11 | TError = CompatibleError, 12 | TContext = unknown 13 | >( 14 | defaultOptions: CreateMutationOptions 15 | ): MutationHook { 16 | return Object.assign( 17 | withMiddleware(ReactQuery.useMutation, defaultOptions, 'mutations'), 18 | { 19 | getKey: () => defaultOptions.mutationKey, 20 | getOptions: () => defaultOptions, 21 | mutationFn: defaultOptions.mutationFn, 22 | } 23 | ) as MutationHook 24 | } 25 | -------------------------------------------------------------------------------- /src/createQuery.ts: -------------------------------------------------------------------------------- 1 | import { createBaseQuery } from './createBaseQuery' 2 | import type { CompatibleError, CreateQueryOptions, QueryHook } from './types' 3 | import { ReactQuery } from './utils' 4 | 5 | export function createQuery< 6 | TFnData, 7 | TVariables = void, 8 | TError = CompatibleError 9 | >( 10 | options: CreateQueryOptions 11 | ): QueryHook { 12 | return createBaseQuery(options, ReactQuery.useQuery) 13 | } 14 | -------------------------------------------------------------------------------- /src/createSuspenseInfiniteQuery.ts: -------------------------------------------------------------------------------- 1 | import { createBaseQuery } from './createBaseQuery' 2 | import type { 3 | CompatibleError, 4 | CreateSuspenseInfiniteQueryOptions, 5 | SuspenseInfiniteQueryHook, 6 | } from './types' 7 | import { ReactQuery, isV5, suspenseOptions } from './utils' 8 | 9 | export function createSuspenseInfiniteQuery< 10 | TFnData, 11 | TVariables = void, 12 | TError = CompatibleError, 13 | TPageParam = number 14 | >( 15 | options: CreateSuspenseInfiniteQueryOptions< 16 | TFnData, 17 | TVariables, 18 | TError, 19 | TPageParam 20 | > 21 | ): SuspenseInfiniteQueryHook { 22 | return isV5 23 | ? createBaseQuery(options, ReactQuery.useSuspenseInfiniteQuery) 24 | : createBaseQuery(options, ReactQuery.useInfiniteQuery, suspenseOptions) 25 | } 26 | -------------------------------------------------------------------------------- /src/createSuspenseQuery.ts: -------------------------------------------------------------------------------- 1 | import { createBaseQuery } from './createBaseQuery' 2 | import type { 3 | CompatibleError, 4 | CreateSuspenseQueryOptions, 5 | SuspenseQueryHook, 6 | } from './types' 7 | import { ReactQuery, isV5, suspenseOptions } from './utils' 8 | 9 | export function createSuspenseQuery< 10 | TFnData, 11 | TVariables = void, 12 | TError = CompatibleError 13 | >( 14 | options: CreateSuspenseQueryOptions 15 | ): SuspenseQueryHook { 16 | return isV5 17 | ? createBaseQuery(options, ReactQuery.useSuspenseQuery) 18 | : createBaseQuery(options, ReactQuery.useQuery, suspenseOptions) 19 | } 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createQuery' 2 | export * from './createSuspenseQuery' 3 | export * from './createInfiniteQuery' 4 | export * from './createSuspenseInfiniteQuery' 5 | export * from './createMutation' 6 | export * from './types' 7 | export * from './router' 8 | export { getKey } from './utils' 9 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import { QueryKey } from '@tanstack/react-query' 2 | 3 | import { createInfiniteQuery } from './createInfiniteQuery' 4 | import { createMutation } from './createMutation' 5 | import { createQuery } from './createQuery' 6 | import { createSuspenseInfiniteQuery } from './createSuspenseInfiniteQuery' 7 | import { createSuspenseQuery } from './createSuspenseQuery' 8 | import type { 9 | CompatibleError, 10 | CreateRouter, 11 | RouterConfig, 12 | RouterInfiniteQuery, 13 | RouterInfiniteQueryOptions, 14 | RouterMutation, 15 | RouterMutationOptions, 16 | RouterQuery, 17 | RouterQueryOptions, 18 | } from './types' 19 | 20 | const buildRouter = (keys: QueryKey, config: RouterConfig) => { 21 | return Object.entries(config).reduce( 22 | (acc, [key, opts]) => { 23 | if (!opts._type) { 24 | acc[key] = buildRouter([...keys, key], opts) 25 | } else { 26 | const options: any = { 27 | ...opts, 28 | [opts._type === `m` ? `mutationKey` : `queryKey`]: [...keys, key], 29 | } 30 | 31 | acc[key] = 32 | opts._type === `m` 33 | ? { 34 | useMutation: createMutation(options), 35 | ...createMutation(options), 36 | } 37 | : opts._type === `q` 38 | ? { 39 | useQuery: createQuery(options), 40 | useSuspenseQuery: createSuspenseQuery(options), 41 | ...createQuery(options), 42 | } 43 | : { 44 | useInfiniteQuery: createInfiniteQuery(options), 45 | useSuspenseInfiniteQuery: createSuspenseInfiniteQuery(options), 46 | ...createInfiniteQuery(options), 47 | } 48 | } 49 | 50 | return acc 51 | }, 52 | { 53 | getKey: () => keys, 54 | } as any 55 | ) 56 | } 57 | 58 | export const router = ( 59 | key: string | QueryKey, 60 | config: TConfig 61 | ): CreateRouter => { 62 | return buildRouter(Array.isArray(key) ? key : [key], config) 63 | } 64 | 65 | function query( 66 | options: RouterQueryOptions 67 | ) { 68 | return { 69 | ...options, 70 | _type: 'q', 71 | } as RouterQuery 72 | } 73 | 74 | function infiniteQuery< 75 | TFnData, 76 | TVariables = void, 77 | TError = CompatibleError, 78 | TPageParam = number 79 | >( 80 | options: RouterInfiniteQueryOptions 81 | ) { 82 | return { ...options, _type: 'inf' } as RouterInfiniteQuery< 83 | TFnData, 84 | TVariables, 85 | TError, 86 | TPageParam 87 | > 88 | } 89 | 90 | function mutation< 91 | TFnData = unknown, 92 | TVariables = void, 93 | TError = CompatibleError, 94 | TContext = unknown 95 | >(options: RouterMutationOptions) { 96 | return { ...options, _type: 'm' } as RouterMutation< 97 | TFnData, 98 | TVariables, 99 | TError, 100 | TContext 101 | > 102 | } 103 | 104 | router.query = query 105 | router.infiniteQuery = infiniteQuery 106 | router.mutation = mutation 107 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DataTag, 3 | DefaultError, 4 | DefinedUseInfiniteQueryResult, 5 | DefinedUseQueryResult, 6 | InfiniteData, 7 | InfiniteQueryObserverSuccessResult, 8 | MutationFunction, 9 | MutationKey, 10 | QueryClient, 11 | QueryFunction, 12 | QueryFunctionContext, 13 | QueryKey, 14 | QueryKeyHashFunction, 15 | QueryObserverSuccessResult, 16 | SkipToken, 17 | UseInfiniteQueryOptions, 18 | UseInfiniteQueryResult, 19 | UseMutationOptions, 20 | UseMutationResult, 21 | UseQueryOptions, 22 | UseQueryResult, 23 | } from '@tanstack/react-query' 24 | 25 | // utils 26 | 27 | type CompatibleWithV4 = 28 | InfiniteData extends UseInfiniteQueryResult< 29 | InfiniteData 30 | >['data'] 31 | ? V5 32 | : V4 33 | 34 | type CompatibleUseInfiniteQueryOptions = 35 | CompatibleWithV4< 36 | UseInfiniteQueryOptions< 37 | TFnData, 38 | TError, 39 | TData, 40 | TFnData, 41 | QueryKey, 42 | TPageParam 43 | >, 44 | UseInfiniteQueryOptions 45 | > 46 | 47 | type CompatibleInfiniteData = CompatibleWithV4< 48 | InfiniteData, 49 | InfiniteData 50 | > 51 | 52 | type NonUndefinedGuard = T extends undefined ? never : T 53 | 54 | type WithRequired = T & { 55 | [_ in K]: {} 56 | } 57 | 58 | type DeepPartial = T extends object 59 | ? { 60 | [P in keyof T]?: DeepPartial 61 | } 62 | : T 63 | 64 | type DefaultTo = unknown extends T ? D : T 65 | 66 | export type CompatibleError = CompatibleWithV4 67 | 68 | export type Fetcher = ( 69 | variables: TVariables, 70 | context: QueryFunctionContext 71 | ) => TFnData | Promise 72 | 73 | export type AdditionalQueryOptions = { 74 | fetcher: Fetcher 75 | variables?: TVariables 76 | } 77 | 78 | type inferMiddlewareHook any> = ( 79 | options: inferCreateOptions, 80 | queryClient?: CompatibleWithV4 81 | ) => ReturnType 82 | 83 | export type Middleware< 84 | T extends (...args: any) => any = QueryHook 85 | > = (hook: inferMiddlewareHook) => inferMiddlewareHook 86 | 87 | export type ExposeFetcher = ( 88 | variables: TVariables, 89 | context?: Partial> 90 | ) => TFnData | Promise 91 | 92 | export type ExposeMethods = { 93 | fetcher: ExposeFetcher 94 | getKey: ( 95 | variables?: DeepPartial 96 | ) => CompatibleWithV4< 97 | DataTag< 98 | QueryKey, 99 | [TPageParam] extends [never] ? TFnData : InfiniteData 100 | >, 101 | QueryKey 102 | > 103 | getFetchOptions: ( 104 | variables: TVariables extends void 105 | ? CompatibleWithV4 | void 106 | : CompatibleWithV4 107 | ) => Pick< 108 | ReturnType< 109 | ExposeMethods['getOptions'] 110 | >, 111 | // @ts-ignore 112 | [TPageParam] extends [never] 113 | ? 'queryKey' | 'queryFn' | 'queryKeyHashFn' 114 | : 115 | | 'queryKey' 116 | | 'queryFn' 117 | | 'queryKeyHashFn' 118 | | 'getNextPageParam' 119 | | 'getPreviousPageParam' 120 | | 'initialPageParam' 121 | > 122 | getOptions: ( 123 | variables: TVariables extends void 124 | ? CompatibleWithV4 | void 125 | : CompatibleWithV4 126 | ) => [TPageParam] extends [never] 127 | ? CompatibleWithV4< 128 | UseQueryOptions & { 129 | queryKey: DataTag 130 | queryFn?: Exclude< 131 | UseQueryOptions['queryFn'], 132 | SkipToken 133 | > 134 | }, 135 | // Not work to infer TError in v4 136 | { 137 | queryKey: QueryKey 138 | queryFn: QueryFunction 139 | queryKeyHashFn?: QueryKeyHashFunction 140 | } 141 | > 142 | : CompatibleUseInfiniteQueryOptions< 143 | TFnData, 144 | TFnData, 145 | TError, 146 | TPageParam 147 | > & { 148 | queryKey: CompatibleWithV4< 149 | DataTag>, 150 | QueryKey 151 | > 152 | } 153 | } 154 | 155 | type Clone = T extends infer TClone ? TClone : never 156 | 157 | // query hook 158 | 159 | export interface CreateQueryOptions< 160 | TFnData = unknown, 161 | TVariables = void, 162 | TError = CompatibleError 163 | > extends Omit< 164 | UseQueryOptions, 165 | 'queryKey' | 'queryFn' | 'select' 166 | >, 167 | AdditionalQueryOptions { 168 | queryKey: QueryKey 169 | use?: Middleware< 170 | QueryHook, Clone, Clone> 171 | >[] 172 | variables?: TVariables 173 | } 174 | 175 | export interface QueryHookOptions 176 | extends Omit< 177 | UseQueryOptions, 178 | 'queryKey' | 'queryFn' | 'queryKeyHashFn' 179 | > { 180 | use?: Middleware>[] 181 | variables?: CompatibleWithV4 182 | } 183 | 184 | export interface DefinedQueryHookOptions 185 | extends Omit< 186 | QueryHookOptions, 187 | 'initialData' 188 | > { 189 | initialData: NonUndefinedGuard | (() => NonUndefinedGuard) 190 | } 191 | 192 | export type QueryHookResult = UseQueryResult 193 | 194 | export type DefinedQueryHookResult = DefinedUseQueryResult< 195 | TData, 196 | TError 197 | > 198 | 199 | export interface QueryHook< 200 | TFnData = unknown, 201 | TVariables = void, 202 | TError = CompatibleError 203 | > extends ExposeMethods { 204 | ( 205 | options: DefinedQueryHookOptions, 206 | queryClient?: CompatibleWithV4 207 | ): DefinedQueryHookResult 208 | ( 209 | options?: QueryHookOptions, 210 | queryClient?: CompatibleWithV4 211 | ): QueryHookResult 212 | } 213 | 214 | // suspense query hook 215 | 216 | export interface CreateSuspenseQueryOptions< 217 | TFnData = unknown, 218 | TVariables = void, 219 | TError = CompatibleError 220 | > extends Omit< 221 | UseQueryOptions, 222 | | 'queryKey' 223 | | 'queryFn' 224 | | 'enabled' 225 | | 'select' 226 | | 'suspense' 227 | | 'throwOnError' 228 | | 'placeholderData' 229 | | 'keepPreviousData' 230 | | 'useErrorBoundary' 231 | >, 232 | AdditionalQueryOptions { 233 | queryKey: QueryKey 234 | use?: Middleware< 235 | SuspenseQueryHook, Clone, Clone> 236 | >[] 237 | variables?: TVariables 238 | } 239 | 240 | export interface SuspenseQueryHookOptions 241 | extends Omit< 242 | UseQueryOptions, 243 | | 'queryKey' 244 | | 'queryFn' 245 | | 'queryKeyHashFn' 246 | | 'enabled' 247 | | 'suspense' 248 | | 'throwOnError' 249 | | 'placeholderData' 250 | | 'keepPreviousData' 251 | | 'useErrorBoundary' 252 | > { 253 | use?: Middleware>[] 254 | variables?: CompatibleWithV4 255 | } 256 | 257 | export type SuspenseQueryHookResult = Omit< 258 | QueryObserverSuccessResult, 259 | 'isPlaceholderData' | 'isPreviousData' 260 | > 261 | 262 | export interface SuspenseQueryHook< 263 | TFnData = unknown, 264 | TVariables = void, 265 | TError = CompatibleError 266 | > extends ExposeMethods { 267 | ( 268 | options?: SuspenseQueryHookOptions, 269 | queryClient?: CompatibleWithV4 270 | ): SuspenseQueryHookResult 271 | } 272 | 273 | // infinite query hook 274 | 275 | export interface CreateInfiniteQueryOptions< 276 | TFnData = unknown, 277 | TVariables = void, 278 | TError = CompatibleError, 279 | TPageParam = number 280 | > extends Omit< 281 | CompatibleUseInfiniteQueryOptions, 282 | 'queryKey' | 'queryFn' | 'select' 283 | >, 284 | AdditionalQueryOptions { 285 | queryKey: QueryKey 286 | use?: Middleware< 287 | InfiniteQueryHook< 288 | Clone, 289 | Clone, 290 | Clone, 291 | Clone 292 | > 293 | >[] 294 | variables?: TVariables 295 | } 296 | 297 | export interface InfiniteQueryHookOptions< 298 | TFnData, 299 | TError, 300 | TData, 301 | TVariables, 302 | TPageParam = number 303 | > extends Omit< 304 | CompatibleUseInfiniteQueryOptions, 305 | | 'queryKey' 306 | | 'queryFn' 307 | | 'queryKeyHashFn' 308 | | 'initialPageParam' 309 | | 'getPreviousPageParam' 310 | | 'getNextPageParam' 311 | > { 312 | use?: Middleware>[] 313 | variables?: CompatibleWithV4 314 | } 315 | 316 | export interface DefinedInfiniteQueryHookOptions< 317 | TFnData, 318 | TError, 319 | TData, 320 | TVariables, 321 | TPageParam = number 322 | > extends Omit< 323 | InfiniteQueryHookOptions, 324 | 'initialData' 325 | > { 326 | initialData: 327 | | NonUndefinedGuard> 328 | | (() => NonUndefinedGuard>) 329 | } 330 | 331 | export type InfiniteQueryHookResult = UseInfiniteQueryResult< 332 | TData, 333 | TError 334 | > 335 | 336 | export type DefinedInfiniteQueryHookResult = CompatibleWithV4< 337 | DefinedUseInfiniteQueryResult, 338 | WithRequired, 'data'> 339 | > 340 | 341 | export interface InfiniteQueryHook< 342 | TFnData = unknown, 343 | TVariables = void, 344 | TError = CompatibleError, 345 | TPageParam = number 346 | > extends ExposeMethods { 347 | , TFnData>>( 348 | options: DefinedInfiniteQueryHookOptions< 349 | TFnData, 350 | TError, 351 | TData, 352 | TVariables, 353 | TPageParam 354 | >, 355 | queryClient?: CompatibleWithV4 356 | ): DefinedInfiniteQueryHookResult 357 | , TFnData>>( 358 | options?: InfiniteQueryHookOptions< 359 | TFnData, 360 | TError, 361 | TData, 362 | TVariables, 363 | TPageParam 364 | >, 365 | queryClient?: CompatibleWithV4 366 | ): InfiniteQueryHookResult 367 | } 368 | 369 | // infinite sususpense query hook 370 | 371 | export interface CreateSuspenseInfiniteQueryOptions< 372 | TFnData = unknown, 373 | TVariables = void, 374 | TError = CompatibleError, 375 | TPageParam = number 376 | > extends Omit< 377 | CompatibleUseInfiniteQueryOptions, 378 | | 'queryKey' 379 | | 'queryFn' 380 | | 'enabled' 381 | | 'select' 382 | | 'suspense' 383 | | 'throwOnError' 384 | | 'placeholderData' 385 | | 'keepPreviousData' 386 | | 'useErrorBoundary' 387 | >, 388 | AdditionalQueryOptions { 389 | queryKey: QueryKey 390 | use?: Middleware< 391 | SuspenseInfiniteQueryHook< 392 | Clone, 393 | Clone, 394 | Clone, 395 | Clone 396 | > 397 | >[] 398 | variables?: TVariables 399 | } 400 | 401 | export interface SuspenseInfiniteQueryHookOptions< 402 | TFnData, 403 | TError, 404 | TData, 405 | TVariables, 406 | TPageParam = number 407 | > extends Omit< 408 | CompatibleUseInfiniteQueryOptions, 409 | | 'queryKey' 410 | | 'queryFn' 411 | | 'queryKeyHashFn' 412 | | 'enabled' 413 | | 'initialPageParam' 414 | | 'getPreviousPageParam' 415 | | 'getNextPageParam' 416 | | 'suspense' 417 | | 'throwOnError' 418 | | 'placeholderData' 419 | | 'keepPreviousData' 420 | | 'useErrorBoundary' 421 | > { 422 | use?: Middleware>[] 423 | variables?: CompatibleWithV4 424 | } 425 | 426 | export type SuspenseInfiniteQueryHookResult = Omit< 427 | InfiniteQueryObserverSuccessResult, 428 | 'isPlaceholderData' | 'isPreviousData' 429 | > 430 | 431 | export interface SuspenseInfiniteQueryHook< 432 | TFnData = unknown, 433 | TVariables = void, 434 | TError = CompatibleError, 435 | TPageParam = number 436 | > extends ExposeMethods { 437 | , TFnData>>( 438 | options?: SuspenseInfiniteQueryHookOptions< 439 | TFnData, 440 | TError, 441 | TData, 442 | TVariables, 443 | TPageParam 444 | >, 445 | queryClient?: CompatibleWithV4 446 | ): SuspenseInfiniteQueryHookResult 447 | } 448 | 449 | // mutation hook 450 | 451 | export interface CreateMutationOptions< 452 | TData = unknown, 453 | TVariables = void, 454 | TError = CompatibleError, 455 | TContext = unknown 456 | > extends UseMutationOptions { 457 | use?: Middleware>[] 458 | } 459 | 460 | export interface MutationHookOptions 461 | extends Omit< 462 | UseMutationOptions, 463 | 'mutationFn' | 'mutationKey' 464 | > { 465 | use?: Middleware>[] 466 | } 467 | 468 | export type MutationHookResult< 469 | TData = unknown, 470 | TError = CompatibleError, 471 | TVariables = void, 472 | TContext = unknown 473 | > = UseMutationResult 474 | 475 | export interface ExposeMutationMethods< 476 | TData = unknown, 477 | TVariables = void, 478 | TError = CompatibleError, 479 | TDefaultContext = unknown 480 | > { 481 | getKey: () => MutationKey | undefined 482 | getOptions: () => UseMutationOptions< 483 | TData, 484 | TError, 485 | TVariables, 486 | TDefaultContext 487 | > 488 | mutationFn: MutationFunction 489 | } 490 | 491 | export interface MutationHook< 492 | TData = unknown, 493 | TVariables = void, 494 | TError = CompatibleError, 495 | TDefaultContext = unknown 496 | > extends ExposeMutationMethods { 497 | ( 498 | options?: MutationHookOptions, 499 | queryClient?: CompatibleWithV4 500 | ): MutationHookResult 501 | } 502 | 503 | // infer types 504 | 505 | export type inferVariables = T extends { 506 | fetcher: ExposeFetcher 507 | } 508 | ? TVariables 509 | : T extends ExposeMutationMethods 510 | ? TVariables 511 | : never 512 | 513 | export type inferData = T extends { 514 | fetcher: ExposeFetcher 515 | } 516 | ? [TPageParam] extends [never] 517 | ? TFnData 518 | : CompatibleInfiniteData 519 | : T extends ExposeMutationMethods 520 | ? TFnData 521 | : never 522 | 523 | export type inferFnData = T extends { 524 | fetcher: ExposeFetcher 525 | } 526 | ? TFnData 527 | : T extends ExposeMutationMethods 528 | ? TFnData 529 | : never 530 | 531 | export type inferError = T extends ExposeMethods 532 | ? TError 533 | : T extends ExposeMethods 534 | ? TError 535 | : T extends ExposeMutationMethods 536 | ? TError 537 | : never 538 | 539 | export type inferOptions = T extends QueryHook< 540 | infer TFnData, 541 | infer TVariables, 542 | infer TError 543 | > 544 | ? QueryHookOptions 545 | : T extends SuspenseQueryHook 546 | ? SuspenseQueryHookOptions 547 | : T extends InfiniteQueryHook< 548 | infer TFnData, 549 | infer TVariables, 550 | infer TError, 551 | infer TPageParam 552 | > 553 | ? InfiniteQueryHookOptions< 554 | TFnData, 555 | TError, 556 | CompatibleWithV4, TFnData>, 557 | TVariables, 558 | TPageParam 559 | > 560 | : T extends SuspenseInfiniteQueryHook< 561 | infer TFnData, 562 | infer TVariables, 563 | infer TError, 564 | infer TPageParam 565 | > 566 | ? SuspenseInfiniteQueryHookOptions< 567 | TFnData, 568 | TError, 569 | CompatibleWithV4, TFnData>, 570 | TVariables, 571 | TPageParam 572 | > 573 | : T extends MutationHook 574 | ? MutationHookOptions 575 | : never 576 | 577 | export type inferCreateOptions = T extends QueryHook< 578 | infer TFnData, 579 | infer TVariables, 580 | infer TError 581 | > 582 | ? CreateQueryOptions 583 | : T extends SuspenseQueryHook 584 | ? CreateSuspenseQueryOptions 585 | : T extends InfiniteQueryHook< 586 | infer TFnData, 587 | infer TVariables, 588 | infer TError, 589 | infer TPageParam 590 | > 591 | ? CreateInfiniteQueryOptions 592 | : T extends SuspenseInfiniteQueryHook< 593 | infer TFnData, 594 | infer TVariables, 595 | infer TError, 596 | infer TPageParam 597 | > 598 | ? CreateSuspenseInfiniteQueryOptions 599 | : T extends MutationHook< 600 | infer TFnData, 601 | infer TVariables, 602 | infer TError, 603 | infer TContext 604 | > 605 | ? CreateMutationOptions 606 | : never 607 | 608 | // router 609 | 610 | export type RouterQueryOptions< 611 | TFnData, 612 | TVariables = void, 613 | TError = CompatibleError 614 | > = Omit, 'queryKey'> 615 | 616 | export type RouterQuery< 617 | TFnData, 618 | TVariables = void, 619 | TError = CompatibleError 620 | > = RouterQueryOptions & { 621 | _type: `q` 622 | } 623 | 624 | export type ResolvedRouterQuery< 625 | TFnData, 626 | TVariables = void, 627 | TError = CompatibleError 628 | > = { 629 | useQuery: QueryHook 630 | useSuspenseQuery: SuspenseQueryHook 631 | } & ExposeMethods 632 | 633 | export type RouterInfiniteQueryOptions< 634 | TFnData, 635 | TVariables = void, 636 | TError = CompatibleError, 637 | TPageParam = number 638 | > = Omit< 639 | CreateInfiniteQueryOptions, 640 | 'queryKey' 641 | > 642 | 643 | export type RouterInfiniteQuery< 644 | TFnData, 645 | TVariables = void, 646 | TError = CompatibleError, 647 | TPageParam = number 648 | > = RouterInfiniteQueryOptions< 649 | TFnData, 650 | TVariables, 651 | TError, 652 | Clone 653 | > & { 654 | _type: `inf` 655 | } 656 | 657 | export type ResolvedRouterInfiniteQuery< 658 | TFnData, 659 | TVariables = void, 660 | TError = CompatibleError, 661 | TPageParam = number 662 | > = { 663 | useInfiniteQuery: InfiniteQueryHook 664 | useSuspenseInfiniteQuery: SuspenseInfiniteQueryHook< 665 | TFnData, 666 | TVariables, 667 | TError, 668 | TPageParam 669 | > 670 | } & ExposeMethods 671 | 672 | export type RouterMutationOptions< 673 | TData = unknown, 674 | TVariables = void, 675 | TError = CompatibleError, 676 | TContext = unknown 677 | > = Omit< 678 | CreateMutationOptions, 679 | 'mutationKey' 680 | > 681 | 682 | export type RouterMutation< 683 | TData = unknown, 684 | TVariables = void, 685 | TError = CompatibleError, 686 | TContext = unknown 687 | > = RouterMutationOptions & { 688 | _type: `m` 689 | } 690 | 691 | export type ResolvedRouterMutation< 692 | TData = unknown, 693 | TVariables = void, 694 | TError = CompatibleError, 695 | TContext = unknown 696 | > = { 697 | useMutation: MutationHook< 698 | TData, 699 | DefaultTo, 700 | DefaultTo, 701 | TContext 702 | > 703 | } & ExposeMutationMethods< 704 | TData, 705 | DefaultTo, 706 | DefaultTo, 707 | TContext 708 | > 709 | 710 | export interface RouterConfig { 711 | [k: string]: 712 | | RouterQuery 713 | | RouterInfiniteQuery 714 | | RouterMutation 715 | | RouterConfig 716 | } 717 | 718 | export type CreateRouter = { 719 | [K in keyof TConfig]: TConfig[K] extends RouterMutation< 720 | infer TFnData, 721 | infer TVariables, 722 | infer TError, 723 | infer TContext 724 | > 725 | ? ResolvedRouterMutation< 726 | TFnData, 727 | DefaultTo, 728 | DefaultTo, 729 | TContext 730 | > 731 | : TConfig[K] extends RouterInfiniteQuery< 732 | infer TFnData, 733 | infer TVariables, 734 | infer TError, 735 | infer TPageParam 736 | > 737 | ? ResolvedRouterInfiniteQuery< 738 | TFnData, 739 | DefaultTo, 740 | DefaultTo, 741 | DefaultTo 742 | > 743 | : TConfig[K] extends RouterQuery< 744 | infer TFnData, 745 | infer TVariables, 746 | infer TError 747 | > 748 | ? ResolvedRouterQuery< 749 | TFnData, 750 | DefaultTo, 751 | DefaultTo 752 | > 753 | : TConfig[K] extends RouterConfig 754 | ? CreateRouter 755 | : never 756 | } & { getKey: () => QueryKey } 757 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as TanstackReactQuery from '@tanstack/react-query' 2 | import type { Query, QueryClient, QueryKey } from '@tanstack/react-query' 3 | 4 | import type { Middleware } from './types' 5 | 6 | export const ReactQuery = TanstackReactQuery 7 | 8 | export const isV5 = !!ReactQuery.useSuspenseQuery 9 | 10 | export const suspenseOptions = { 11 | enabled: true, 12 | suspense: true, 13 | keepPreviousData: undefined, 14 | useErrorBoundary: (_error: unknown, query: Query) => 15 | query.state.data === undefined, 16 | } 17 | 18 | export const withMiddleware = ( 19 | hook: any, 20 | defaultOptions: any, 21 | type: 'queries' | 'mutations' 22 | ) => { 23 | return function useMiddleware( 24 | options?: { client?: QueryClient; use?: Middleware[] }, 25 | queryClient?: QueryClient 26 | ) { 27 | const [uses, opts]: [Middleware[], any] = [ 28 | ReactQuery.useQueryClient( 29 | // @ts-ignore Compatible with ReactQuery v4 30 | isV5 ? queryClient : options 31 | ).getDefaultOptions()[type], 32 | defaultOptions, 33 | options, 34 | ].reduce( 35 | ([u1, o1], { use: u2 = [], ...o2 } = {}) => [ 36 | [...u1, ...u2], 37 | { ...o1, ...o2 }, 38 | ], 39 | [[]] 40 | ) 41 | 42 | return uses.reduceRight((next, use) => use(next), hook)(opts, queryClient) 43 | } 44 | } 45 | 46 | export const getKey = (queryKey: QueryKey, variables?: any): QueryKey => { 47 | return variables === undefined ? queryKey : [...queryKey, variables] 48 | } 49 | -------------------------------------------------------------------------------- /tests/createInfiniteQuery.test.tsx: -------------------------------------------------------------------------------- 1 | import { createInfiniteQuery } from '../src/createInfiniteQuery' 2 | import { omit, uniqueKey } from './utils' 3 | 4 | describe('createInfiniteQuery', () => { 5 | it('should return the correct key', () => { 6 | type Response = { 7 | projects: { id: string; name: string }[] 8 | nextCursor: number 9 | } 10 | type Variables = { id: number } 11 | 12 | const key = uniqueKey() 13 | const variables = { id: 1 } 14 | const fetcher = (_variables: Variables): Promise => { 15 | return fetch(`/test`).then(res => res.json()) 16 | } 17 | const initialPageParam = 1 18 | const getNextPageParam = (lastPage: Response) => lastPage.nextCursor 19 | const useGeneratedQuery = createInfiniteQuery({ 20 | queryKey: key, 21 | fetcher, 22 | initialPageParam, 23 | getNextPageParam, 24 | }) 25 | 26 | expect(useGeneratedQuery.getKey()).toEqual(key) 27 | expect(useGeneratedQuery.getKey(variables)).toEqual([...key, variables]) 28 | expect(omit(useGeneratedQuery.getOptions(variables), 'queryFn')).toEqual({ 29 | queryKey: [...key, variables], 30 | fetcher, 31 | initialPageParam, 32 | getNextPageParam, 33 | }) 34 | expect( 35 | omit(useGeneratedQuery.getFetchOptions(variables), 'queryFn') 36 | ).toEqual({ 37 | queryKey: [...key, variables], 38 | initialPageParam, 39 | getNextPageParam, 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/createMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import { type MutationKey } from '@tanstack/react-query' 2 | 3 | import { createMutation } from '../src/createMutation' 4 | 5 | describe('createMutation', () => { 6 | it('should return the correct key', () => { 7 | const mutationKey: MutationKey = ['mutationKey'] 8 | const mutation = createMutation({ 9 | mutationKey, 10 | mutationFn: async () => mutationKey, 11 | }) 12 | 13 | expect(mutation.getKey()).toEqual(mutationKey) 14 | mutation.mutationFn().then(data => expect(data).toEqual(mutationKey)) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /tests/createQuery.test.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, skipToken } from '@tanstack/react-query' 2 | import '@testing-library/jest-dom' 3 | import { fireEvent, waitFor } from '@testing-library/react' 4 | import * as React from 'react' 5 | 6 | import { createQuery } from '../src' 7 | import type { QueryHookResult } from '../src' 8 | import { Middleware } from '../src/types' 9 | import { omit, renderWithClient, sleep, uniqueKey } from './utils' 10 | 11 | describe('createQuery', () => { 12 | const queryClient = new QueryClient() 13 | 14 | it('should return the correct key', () => { 15 | const key = uniqueKey() 16 | const variables = { id: 1 } 17 | const fetcher = (_variables: { id: number }) => { 18 | return 'test' 19 | } 20 | const useGeneratedQuery = createQuery({ 21 | queryKey: key, 22 | fetcher, 23 | }) 24 | 25 | expect(useGeneratedQuery.getKey()).toEqual(key) 26 | expect(useGeneratedQuery.getKey(variables)).toEqual([...key, variables]) 27 | expect(omit(useGeneratedQuery.getOptions(variables), 'queryFn')).toEqual({ 28 | queryKey: [...key, variables], 29 | fetcher, 30 | }) 31 | expect( 32 | omit(useGeneratedQuery.getFetchOptions(variables), 'queryFn') 33 | ).toEqual({ 34 | queryKey: [...key, variables], 35 | }) 36 | 37 | queryClient.prefetchQuery(useGeneratedQuery.getFetchOptions(variables)) 38 | }) 39 | 40 | it('should return the correct initial data from middleware', async () => { 41 | const myMiddileware: Middleware = useQueryNext => { 42 | return options => { 43 | return useQueryNext({ 44 | ...options, 45 | initialData: 'initialData', 46 | enabled: false, 47 | }) 48 | } 49 | } 50 | 51 | const useGeneratedQuery = createQuery({ 52 | queryKey: uniqueKey(), 53 | fetcher: (_variables: { id: number }) => { 54 | return 'test' 55 | }, 56 | use: [ 57 | useNext => { 58 | return options => 59 | useNext({ 60 | ...options, 61 | initialData: 'fakeData', 62 | enabled: false, 63 | }) 64 | }, 65 | myMiddileware, 66 | ], 67 | }) 68 | 69 | const states: QueryHookResult[] = [] 70 | 71 | function Page() { 72 | const state = useGeneratedQuery() 73 | 74 | states.push(state) 75 | 76 | return {state.data} 77 | } 78 | 79 | const rendered = renderWithClient(queryClient, ) 80 | 81 | await waitFor(() => rendered.getByText('initialData')) 82 | }) 83 | 84 | it('should return the correct initial data', async () => { 85 | const useGeneratedQuery = createQuery({ 86 | queryKey: uniqueKey(), 87 | fetcher: () => { 88 | return 'test' 89 | }, 90 | use: [ 91 | useNext => { 92 | return options => 93 | useNext({ 94 | ...options, 95 | initialData: options.initialData ?? 'initialData', 96 | enabled: false, 97 | }) 98 | }, 99 | ], 100 | }) 101 | const states: QueryHookResult[] = [] 102 | 103 | function Page() { 104 | const state = useGeneratedQuery({ initialData: 'stateData' }) 105 | 106 | states.push(state) 107 | 108 | return {state.data} 109 | } 110 | 111 | const rendered = renderWithClient(queryClient, ) 112 | 113 | await waitFor(() => rendered.getByText('stateData')) 114 | }) 115 | 116 | it('should return the selected data', async () => { 117 | const useGeneratedQuery = createQuery({ 118 | queryKey: uniqueKey(), 119 | fetcher: () => { 120 | return 'test' 121 | }, 122 | }) 123 | const states: QueryHookResult[] = [] 124 | 125 | function Page() { 126 | const state = useGeneratedQuery({ 127 | select() { 128 | return 'selectedData' 129 | }, 130 | }) 131 | 132 | states.push(state) 133 | 134 | return {state.data} 135 | } 136 | 137 | const rendered = renderWithClient(queryClient, ) 138 | 139 | await waitFor(() => rendered.getByText('selectedData')) 140 | }) 141 | 142 | it('should respect skipToken and refetch when skipToken is taken away', async () => { 143 | const useGeneratedQuery = createQuery({ 144 | queryKey: uniqueKey(), 145 | fetcher: async () => { 146 | await sleep(10) 147 | return Promise.resolve('data') 148 | }, 149 | }) 150 | 151 | function Page({ enabled }: { enabled: boolean }) { 152 | const { data, status } = useGeneratedQuery({ 153 | variables: enabled ? undefined : skipToken, 154 | retry: false, 155 | retryOnMount: false, 156 | refetchOnMount: false, 157 | refetchOnWindowFocus: false, 158 | }) 159 | 160 | return ( 161 |
162 |
status: {status}
163 |
data: {String(data)}
164 |
165 | ) 166 | } 167 | 168 | function App() { 169 | const [enabled, toggle] = React.useReducer(x => !x, false) 170 | 171 | return ( 172 |
173 | 174 | 175 |
176 | ) 177 | } 178 | 179 | const rendered = renderWithClient(queryClient, ) 180 | 181 | await waitFor(() => rendered.getByText('status: pending')) 182 | 183 | fireEvent.click(rendered.getByRole('button', { name: 'enable' })) 184 | await waitFor(() => rendered.getByText('status: success')) 185 | await waitFor(() => rendered.getByText('data: data')) 186 | }) 187 | }) 188 | -------------------------------------------------------------------------------- /tests/router.test.tsx: -------------------------------------------------------------------------------- 1 | import { router } from '../src' 2 | 3 | describe('router', () => { 4 | it('should return the correct shape', () => { 5 | const post = router(`post`, { 6 | byId: router.query({ 7 | fetcher: (variables: { 8 | id: number 9 | }): Promise<{ title: string; content: string }> => 10 | fetch(`/post/${variables.id}`).then(res => res.json()), 11 | }), 12 | 13 | list: router.infiniteQuery({ 14 | fetcher: (_variables, { pageParam }) => 15 | fetch(`/post/?cursor=${pageParam}`).then(res => res.json()), 16 | getNextPageParam: lastPage => lastPage.nextCursor, 17 | initialPageParam: 0, 18 | }), 19 | 20 | add: router.mutation({ 21 | mutationFn: async (variables: { 22 | title: string 23 | content: string 24 | }): Promise<{ 25 | ret: number 26 | }> => 27 | fetch('/post', { 28 | method: 'POST', 29 | body: JSON.stringify(variables), 30 | }).then(res => res.json()), 31 | }), 32 | 33 | command: { 34 | report: router.query({ 35 | fetcher: (variables: { 36 | id: number 37 | }): Promise<{ title: string; content: string }> => 38 | fetch(`/post/report/${variables.id}`).then(res => res.json()), 39 | }), 40 | }, 41 | }) 42 | 43 | expect(post.getKey()).toEqual(['post']) 44 | expect(post.byId.getKey()).toEqual(['post', 'byId']) 45 | expect(post.byId.getKey({ id: 1 })).toEqual(['post', 'byId', { id: 1 }]) 46 | expect(post.list.getKey()).toEqual(['post', 'list']) 47 | expect(post.add.getKey()).toEqual(['post', 'add']) 48 | expect(post.command.getKey()).toEqual(['post', 'command']) 49 | expect(post.command.report.getKey()).toEqual(['post', 'command', 'report']) 50 | expect(post.command.report.getKey({ id: 1 })).toEqual([ 51 | 'post', 52 | 'command', 53 | 'report', 54 | { id: 1 }, 55 | ]) 56 | expect(typeof post.command.report.fetcher === 'function').toBe(true) 57 | expect(typeof post.command.report.getFetchOptions === 'function').toBe(true) 58 | expect(typeof post.command.report.getOptions === 'function').toBe(true) 59 | expect(typeof post.command.report.useQuery === 'function').toBe(true) 60 | expect(typeof post.command.report.useSuspenseQuery === 'function').toBe( 61 | true 62 | ) 63 | expect(typeof post.byId.fetcher === 'function').toBe(true) 64 | expect(typeof post.byId.getFetchOptions === 'function').toBe(true) 65 | expect(typeof post.byId.getOptions === 'function').toBe(true) 66 | expect(typeof post.byId.useQuery === 'function').toBe(true) 67 | expect(typeof post.byId.useSuspenseQuery === 'function').toBe(true) 68 | expect(typeof post.list.fetcher === 'function').toBe(true) 69 | expect(typeof post.list.getFetchOptions === 'function').toBe(true) 70 | expect(typeof post.list.getOptions === 'function').toBe(true) 71 | expect(typeof post.list.useInfiniteQuery === 'function').toBe(true) 72 | expect(typeof post.list.useSuspenseInfiniteQuery === 'function').toBe(true) 73 | expect(typeof post.add.mutationFn === 'function').toBe(true) 74 | expect(typeof post.add.getKey === 'function').toBe(true) 75 | expect(typeof post.add.getOptions === 'function').toBe(true) 76 | expect(typeof post.add.useMutation === 'function').toBe(true) 77 | }) 78 | 79 | it('should return the correct shape when pass a array keys', () => { 80 | const post = router(['scope', `post`], { 81 | byId: router.query({ 82 | fetcher: (variables: { 83 | id: number 84 | }): Promise<{ title: string; content: string }> => 85 | fetch(`/post/${variables.id}`).then(res => res.json()), 86 | }), 87 | 88 | list: router.infiniteQuery({ 89 | fetcher: (_variables, { pageParam }) => 90 | fetch(`/post/?cursor=${pageParam}`).then(res => res.json()), 91 | getNextPageParam: lastPage => lastPage.nextCursor, 92 | initialPageParam: 0, 93 | }), 94 | 95 | add: router.mutation({ 96 | mutationFn: async (variables: { 97 | title: string 98 | content: string 99 | }): Promise<{ 100 | ret: number 101 | }> => 102 | fetch('/post', { 103 | method: 'POST', 104 | body: JSON.stringify(variables), 105 | }).then(res => res.json()), 106 | }), 107 | 108 | command: { 109 | report: router.query({ 110 | fetcher: (variables: { 111 | id: number 112 | }): Promise<{ title: string; content: string }> => 113 | fetch(`/post/report/${variables.id}`).then(res => res.json()), 114 | }), 115 | }, 116 | }) 117 | 118 | expect(post.getKey()).toEqual(['scope', 'post']) 119 | expect(post.byId.getKey()).toEqual(['scope', 'post', 'byId']) 120 | expect(post.byId.getKey({ id: 1 })).toEqual([ 121 | 'scope', 122 | 'post', 123 | 'byId', 124 | { id: 1 }, 125 | ]) 126 | expect(post.list.getKey()).toEqual(['scope', 'post', 'list']) 127 | expect(post.add.getKey()).toEqual(['scope', 'post', 'add']) 128 | expect(post.command.getKey()).toEqual(['scope', 'post', 'command']) 129 | expect(post.command.report.getKey()).toEqual([ 130 | 'scope', 131 | 'post', 132 | 'command', 133 | 'report', 134 | ]) 135 | expect(post.command.report.getKey({ id: 1 })).toEqual([ 136 | 'scope', 137 | 'post', 138 | 'command', 139 | 'report', 140 | { id: 1 }, 141 | ]) 142 | expect(typeof post.command.report.fetcher === 'function').toBe(true) 143 | expect(typeof post.command.report.getFetchOptions === 'function').toBe(true) 144 | expect(typeof post.command.report.getOptions === 'function').toBe(true) 145 | expect(typeof post.command.report.useQuery === 'function').toBe(true) 146 | expect(typeof post.command.report.useSuspenseQuery === 'function').toBe( 147 | true 148 | ) 149 | expect(typeof post.byId.fetcher === 'function').toBe(true) 150 | expect(typeof post.byId.getFetchOptions === 'function').toBe(true) 151 | expect(typeof post.byId.getOptions === 'function').toBe(true) 152 | expect(typeof post.byId.useQuery === 'function').toBe(true) 153 | expect(typeof post.byId.useSuspenseQuery === 'function').toBe(true) 154 | expect(typeof post.list.fetcher === 'function').toBe(true) 155 | expect(typeof post.list.getFetchOptions === 'function').toBe(true) 156 | expect(typeof post.list.getOptions === 'function').toBe(true) 157 | expect(typeof post.list.useInfiniteQuery === 'function').toBe(true) 158 | expect(typeof post.list.useSuspenseInfiniteQuery === 'function').toBe(true) 159 | expect(typeof post.add.mutationFn === 'function').toBe(true) 160 | expect(typeof post.add.getKey === 'function').toBe(true) 161 | expect(typeof post.add.getOptions === 'function').toBe(true) 162 | expect(typeof post.add.useMutation === 'function').toBe(true) 163 | }) 164 | }) 165 | -------------------------------------------------------------------------------- /tests/utils.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 2 | import { render } from '@testing-library/react' 3 | import * as React from 'react' 4 | 5 | let queryKeyCount = 0 6 | export function uniqueKey(): string[] { 7 | queryKeyCount++ 8 | return [`query_${queryKeyCount}`] 9 | } 10 | 11 | export function renderWithClient( 12 | client: QueryClient, 13 | ui: React.ReactElement 14 | ): ReturnType { 15 | const { rerender, ...result } = render( 16 | {ui} 17 | ) 18 | return { 19 | ...result, 20 | rerender: (rerenderUi: React.ReactElement) => 21 | rerender( 22 | {rerenderUi} 23 | ), 24 | } as any 25 | } 26 | 27 | export function omit( 28 | object: T | null | undefined, 29 | ...paths: K 30 | ): Pick> { 31 | return Object.fromEntries( 32 | Object.entries(object || {}).filter(([key]) => !paths.includes(key)) 33 | ) as Pick> 34 | } 35 | 36 | export function sleep(timeout: number): Promise { 37 | return new Promise((resolve, _reject) => { 38 | setTimeout(resolve, timeout) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "DOM.Iterable", "ES2020"], 4 | "target": "ES2020", 5 | "module": "ES2020", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": false, 11 | "noImplicitThis": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noUncheckedIndexedAccess": true, 15 | "strictNullChecks": true, 16 | "jsx": "react", 17 | "declaration": true, 18 | "emitDeclarationOnly": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "types": ["jest", "node"], 22 | "outDir": "./build/lib" 23 | }, 24 | "files": ["src/index.ts"], 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "./types", 6 | "emitDeclarationOnly": true, 7 | "noEmit": false 8 | }, 9 | "files": ["./src/index.ts"], 10 | "exclude": ["./src/**/*"] 11 | } 12 | --------------------------------------------------------------------------------