├── .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 |
16 |
17 |
18 |
19 |
20 |
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 |
16 |
17 |
18 |
19 |
20 |
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 |
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