Repository: liaoliao666/react-query-kit
Branch: main
Commit: a00020f8ca8c
Files: 35
Total size: 94.4 KB
Directory structure:
gitextract_dtwmuxyq/
├── .browserslistrc
├── .commitlintrc.json
├── .eslintrc
├── .github/
│ └── workflows/
│ ├── publish.yml
│ └── tests.yml
├── .gitignore
├── .husky/
│ └── commit-msg
├── .npmrc
├── .nvmrc
├── LICENSE
├── README-zh_CN.md
├── README.md
├── babel.config.js
├── jest.config.js
├── package.json
├── prettier.config.js
├── rollup.config.js
├── src/
│ ├── createBaseQuery.ts
│ ├── createInfiniteQuery.ts
│ ├── createMutation.ts
│ ├── createQuery.ts
│ ├── createSuspenseInfiniteQuery.ts
│ ├── createSuspenseQuery.ts
│ ├── index.ts
│ ├── router.ts
│ ├── types.ts
│ └── utils.ts
├── tests/
│ ├── createInfiniteQuery.test.tsx
│ ├── createMutation.test.tsx
│ ├── createQuery.test.tsx
│ ├── router.test.tsx
│ ├── types.typecheck.ts
│ └── utils.tsx
├── tsconfig.json
└── tsconfig.types.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .browserslistrc
================================================
# Browsers we support
Chrome >= 73
Firefox >= 78
Edge >= 79
Safari >= 12.0
iOS >= 12.0
opera >= 53
================================================
FILE: .commitlintrc.json
================================================
{
"extends": ["@commitlint/config-conventional"]
}
================================================
FILE: .eslintrc
================================================
{
"env": {
"browser": true,
"shared-node-browser": true,
"node": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended",
"plugin:react-hooks/recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"plugins": [
"@typescript-eslint",
"react",
"prettier",
"react-hooks",
"import",
"jest"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": 2018,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"eqeqeq": "error",
"no-var": "error",
"prefer-const": "error",
"curly": ["warn", "multi-line", "consistent"],
"no-console": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"import/no-unresolved": ["error", { "commonjs": true, "amd": true }],
"import/export": "error",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-duplicate-imports": ["error"],
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
],
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"jest/consistent-test-it": [
"error",
{ "fn": "it", "withinDescribe": "it" }
],
"import/order": "off",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"sort-imports": [
"error",
{
"ignoreDeclarationSort": true
}
]
},
"settings": {
"react": {
"version": "detect"
},
"import/extensions": [".js", ".jsx", ".ts", ".tsx"],
"import/parsers": {
"@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx"]
},
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx", ".json"],
"paths": ["src"]
}
}
},
"overrides": [
{
"files": ["src"],
"parserOptions": {
"project": "./tsconfig.json"
}
},
{
"files": ["tests/**/*.tsx"],
"env": {
"jest/globals": true
}
},
{
"files": ["./*.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
}
]
}
================================================
FILE: .github/workflows/publish.yml
================================================
name: 'publish'
on:
push:
branches:
- main
jobs:
release:
name: publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: .nvmrc
registry-url: https://registry.npmjs.org
cache: 'yarn'
- run: |
git config --global user.name 'liaoliao666'
git config --global user.email '1076988944@qq.com'
yarn
yarn test && yarn build
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_AUTH_TOKEN }}
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
on:
push:
branches:
- main
jobs:
tests:
name: Building package
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Caching node_modules
uses: actions/cache@v3
id: yarn-cache
with:
path: "**/node_modules"
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node-
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies 🔧
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Test package 🔧
run: yarn test
================================================
FILE: .gitignore
================================================
node_modules
# builds
build
# misc
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
package-lock.json
size-plugin.json
stats.json
stats.html
# mac
.DS_Store
================================================
FILE: .husky/commit-msg
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx commitlint --edit
================================================
FILE: .npmrc
================================================
auto-install-peers=true
registry=https://registry.npmjs.org
================================================
FILE: .nvmrc
================================================
v16.19.0
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 liaoliao666
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README-zh_CN.md
================================================
---
## Motivation
- 以类型安全的方式管理 `queryKey`
- 让 `queryClient` 的操作更清楚地关联到哪个自定义 hook
- 可以从任何自定义 ReactQuery hook 中提取的 TypeScript 类型
- 中间件
[English](./README.md) | 简体中文
## Table of Contents
- [安装](#installation)
- [例子](#examples)
- 使用
- [createQuery](#createquery)
- [createInfiniteQuery](#createinfinitequery)
- [createSuspenseQuery](#createsuspensequery)
- [createSuspenseInfiniteQuery](#createsuspenseinfinitequery)
- [createMutation](#createmutation)
- [router](#router)
- [中间件](#中间件)
- [TypeScript](#typescript)
- [类型推导](#类型推导)
- [禁用查询](#禁用查询)
- [常见问题](#常见问题)
- [迁移](#迁移)
- [Issues](#issues)
- [🐛 Bugs](#-bugs)
- [💡 Feature Requests](#-feature-requests)
- [LICENSE](#license)
## Installation
```bash
$ npm i react-query-kit
# or
$ yarn add react-query-kit
```
如果您还在使用 React Query Kit v2? 请在此处查看 v2 文档:https://github.com/liaoliao666/react-query-kit/tree/v2#readme.
# Examples
- [Basic](https://codesandbox.io/s/example-react-query-kit-basic-1ny2j8)
- [Optimistic Updates](https://codesandbox.io/s/example-react-query-kit-optimistic-updates-eefg0v)
- [Next.js](https://codesandbox.io/s/example-react-query-kit-nextjs-uldl88)
- [Load-More & Infinite Scroll](https://codesandbox.io/s/example-react-query-kit-load-more-infinite-scroll-vg494v)
## createQuery
### Usage
```tsx
import { QueryClient, dehydrate } from '@tanstack/react-query'
import { createQuery } from 'react-query-kit'
type Data = { title: string; content: string }
type Variables = { id: number }
const usePost = createQuery({
queryKey: ['posts'],
fetcher: (variables: Variables): Promise => {
return fetch(`/posts/${variables.id}`).then(res => res.json())
},
// 你还可以通过中间件来定制这个 hook 的行为
use: [myMiddleware]
})
const variables = { id: 1 }
// example
export default function Page() {
// queryKey 相等于 ['/posts', { id: 1 }]
const { data } = usePost({ variables })
return (
{data?.title}
{data?.content}
)
}
console.log(usePost.getKey()) // ['/posts']
console.log(usePost.getKey(variables)) // ['/posts', { id: 1 }]
// nextjs 例子
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(usePost.getFetchOptions(variables))
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
// 在 react 组件外使用
const data = await queryClient.fetchQuery(
usePost.getFetchOptions(variables)
)
// useQueries 例子
const queries = useQueries({
queries: [
usePost.getOptions(variables),
useUser.getOptions(),
],
})
// getQueryData
queryClient.getQueryData(usePost.getKey(variables)) // Data
// setQueryData
queryClient.setQueryData(usePost.getKey(variables), {...})
```
### 额外的 API 文档
Options
- `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise`
- 必填
- 用于请求数据的函数。 第二个参数是“queryFn”的“QueryFunctionContext”
- `variables?: TVariables`
- 可选
- `variables` 将是 fetcher 的第一个参数和 `queryKey` 数组的最后一个元素
- `use: Middleware[]`
- 可选
- 中间件函数数组 [(详情)](#中间件)
Expose Methods
- `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise`
- `getKey: (variables: TVariables) => QueryKey`
- `getOptions: (variables: TVariables) => UseInfiniteQueryOptions`
- `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn })`
## createInfiniteQuery
### Usage
```tsx
import { QueryClient, dehydrate } from '@tanstack/react-query'
import { createInfiniteQuery } from 'react-query-kit'
type Data = { projects: { id: string; name: string }[]; nextCursor: number }
type Variables = { active: boolean }
const useProjects = createInfiniteQuery({
queryKey: ['projects'],
fetcher: (variables: Variables, { pageParam }): Promise => {
return fetch(
`/projects?cursor=${pageParam}?active=${variables.active}`
).then(res => res.json())
},
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
initialPageParam: 0,
})
const variables = { active: true }
// example
export default function Page() {
// queryKey equals to ['projects', { active: true }]
const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } =
useProjects({ variables })
return (
{data.pages.map((group, i) => (
{group.projects.map(project => (
{project.name}
))}
))}
fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
{isFetching && !isFetchingNextPage ? 'Fetching...' : null}
)
}
// nextjs example
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchInfiniteQuery(
useProjects.getFetchOptions(variables)
)
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
// 在 react 组件外使用
const data = await queryClient.fetchInfiniteQuery(
useProjects.getFetchOptions(variables)
)
```
### 额外的 API 文档
Options
- `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise`
- 必填
- 查询将用于请求数据的函数。 第二个参数是“queryFn”的“QueryFunctionContext”
- `variables?: TVariables`
- 可选
- `variables` 将是 fetcher 的第一个参数和 `queryKey` 数组的最后一个元素
- `use: Middleware[]`
- 可选
- 中间件函数数组 [(详情)](#中间件)
Expose Methods
- `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise`
- `getKey: (variables: TVariables) => QueryKey`
- `getOptions: (variables: TVariables) => UseInfiniteQueryOptions`
- `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn, getNextPageParam, getPreviousPageParam, initialPageParam })`
## createSuspenseQuery
这与在查询配置中将 suspense 选项设置为 true 具有相同的效果,但在 TypeScript 的体验更好,因为 data 是有定义的(因为错误和加载状态由 Suspense 和 ErrorBoundaries 处理)。
```ts
import { createSuspenseQuery } from 'react-query-kit'
createSuspenseQuery({
...options,
})
// 相当于
createQuery({
...options,
enabled: true,
suspense: true,
throwOnError: true,
})
```
## createSuspenseInfiniteQuery
```ts
import { createSuspenseInfiniteQuery } from 'react-query-kit'
createSuspenseInfiniteQuery({
...options,
})
// 相当于
createInfiniteQuery({
...options,
enabled: true,
suspense: true,
throwOnError: true,
})
```
## createMutation
### Usage
```tsx
import { createMutation } from 'react-query-kit'
const useAddTodo = createMutation(
async (variables: { title: string; content: string }) =>
fetch('/post', {
method: 'POST',
body: JSON.stringify(variables),
}).then(res => res.json()),
{
onSuccess(data, variables, context) {
// do somethings
},
}
)
function App() {
const mutation = useAddTodo({
onSettled: (data, error, variables, context) => {
// Error or success... doesn't matter!
},
})
return (
{mutation.isPending ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
An error occurred: {mutation.error.message}
) : null}
{mutation.isSuccess ?
Todo added!
: null}
{
mutation.mutate({ title: 'Do Laundry', content: 'content...' })
}}
>
Create Todo
>
)}
)
}
// usage outside of react component
useAddTodo.mutationFn({ title: 'Do Laundry', content: 'content...' })
```
### 额外的 API 文档
Options
- `use: Middleware[]`
- 可选
- 中间件函数数组 [(详情)](#中间件)
Expose Methods
- `getKey: () => MutationKey`
- `getOptions: () => UseMutationOptions`
- `mutationFn: ExposeMutationFn`
## router
`router` 允许您创建整个 API 的形状
### Usage
```tsx
import { router } from 'react-query-kit'
const post = router(`post`, {
byId: router.query({
fetcher: (variables: { id: number }) =>
fetch(`/posts/${variables.id}`).then(res => res.json()),
use: [myMiddleware],
}),
list: router.infiniteQuery({
fetcher: (_variables, { pageParam }) =>
fetch(`/posts/?cursor=${pageParam}`).then(res => res.json()),
getNextPageParam: lastPage => lastPage.nextCursor,
initialPageParam: 0,
}),
add: router.mutation({
mutationFn: async (variables: { title: string; content: string }) =>
fetch('/posts', {
method: 'POST',
body: JSON.stringify(variables),
}).then(res => res.json()),
}),
// nest router
command: {
report: router.mutation({ mutationFn }),
promote: router.mutation({ mutationFn }),
},
})
// get root key
post.getKey() // ['post']
// hooks
post.byId.useQuery({ variables: { id: 1 } })
post.byId.useSuspenseQuery({ variables: { id: 1 } })
post.list.useInfiniteQuery()
post.list.useSuspenseInfiniteQuery()
post.add.useMutation()
post.command.report.useMutation()
// expose methods
post.byId.getKey({ id: 1 }) // ['post', 'byId', { id: 1 }]
post.byId.getFetchOptions({ id: 1 })
post.byId.getOptions({ id: 1 })
post.byId.fetcher({ id: 1 })
post.add.getKey() // ['post', 'add']
post.add.getOptions()
post.add.mutationFn({ title: 'title', content: 'content' })
// infer types
type Data = inferData
type FnData = inferFnData
type Variables = inferVariables
type Error = inferError
```
### 合并路由
```ts
import { router } from 'react-query-kit'
const user = router(`user`, {})
const post = router(`post`, {})
const k = {
user,
post,
}
```
### API 文档
`type Router = (key: string | unknown[], config: TConfig) => TRouter`
Expose Methods
- `query`
与 `createQuery` 类似,但无需选项 `queryKey`
- `infiniteQuery`
与 `createInfiniteQuery` 类似,但无需选项 `queryKey`
- `mutation`
与 `createMutation` 类似,但无需选项 `mutationKey`
## 中间件
此功能的灵感来自于 [SWR 的中间件功能](https://swr.vercel.app/docs/middleware)。
中间件接收 hook,可以在运行它之前和之后执行逻辑。如果有多个中间件,则每个中间件包装下一个中间件。列表中的最后一个中间件将接收原始的 hook。
### 使用
```ts
import { QueryClient } from '@tanstack/react-query'
import { Middleware, MutationHook, QueryHook, getKey } from 'react-query-kit'
const logger: Middleware> = useQueryNext => {
return options => {
const log = useLogger()
const fetcher = (variables, context) => {
log(context.queryKey, variables)
return options.fetcher(variables, context)
}
return useQueryNext({
...options,
fetcher,
})
}
}
const useUser = createQuery({
use: [logger],
})
// 全局中间件
const queryMiddleware: Middleware = useQueryNext => {
return options => {
// 你还可以通过函数 getKey 获取 queryKey
const fullKey = getKey(options.queryKey, options.variables)
// ...
return useQueryNext(options)
}
}
const mutationMiddleware: Middleware = useMutationNext => {
return options => {
// ...
return useMutationNext(options)
}
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
use: [queryMiddleware],
},
mutations: {
use: [mutationMiddleware],
},
},
})
```
### 扩展
中间件将从上级合并。例如:
```jsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
use: [a],
},
},
})
const useSomething = createQuery({
use: [b],
})
useSomething({ use: [c] })
```
相当于:
```js
createQuery({ use: [a, b, c] })
```
### 多个中间件
每个中间件包装下一个中间件,最后一个只包装 useQuery hook。例如:
```jsx
createQuery({ use: [a, b, c] })
```
中间件执行的顺序是 `a → b → c`,如下所示:
```plaintext
enter a
enter b
enter c
useQuery()
exit c
exit b
exit a
```
### 多个 QueryClient
在 ReactQuery v5 中,`QueryClient` 将是 `useQuery` 和 `useMutation` 的第二个参数。 如果你在全局中有多个 `QueryClient`,你应该在中间件钩子中接收 `QueryClient`
```ts
const useSomething = createQuery({
use: [
function myMiddleware(useQueryNext) {
// 你应该接收 queryClient 作为第二个参数
return (options, queryClient) => {
const client = useQueryClient(queryClient)
// ...
return useQueryNext(options, queryClient)
}
},
],
})
// 如果你传入另一个 QueryClient
useSomething({...}, anotherQueryClient)
```
## TypeScript
默认情况下,ReactQueryKit 还会从 `fetcher` 推断 `data` 和 `variables` 的类型,因此您可以自动获得首选类型。
```ts
type Data = { title: string; content: string }
type Variables = { id: number }
const usePost = createQuery({
queryKey: ['posts'],
fetcher: (variables: Variables): Promise => {
return fetch(`/posts/${variables}`).then(res => res.json())
},
})
// `data` 将被推断为 `Data | undefined`.
// `variables` 将被推断为 `Variables`.
const { data } = usePost({ variables: { id: 1 } })
```
您还可以显式指定 `fetcher` 参数和返回的类型。
```ts
type Data = { title: string; content: string }
type Variables = { id: number }
const usePost = createQuery({
queryKey: ['posts'],
fetcher: variables => {
return fetch(`/posts/${variables}`).then(res => res.json())
},
})
// `data` 将被推断为 `Data | undefined`.
// `error` 将被推断为 `Error | null`
// `variables` 将被推断为 `Variables`.
const { data, error } = usePost({ variables: { id: 1 } })
```
## 类型推导
您可以使用 `inferData` 或 `inferVariables` 提取任何自定义 hook 的 TypeScript 类型
```ts
import { inferData, inferFnData, inferError, inferVariables, inferOptions } from 'react-query-kit'
const useProjects = createInfiniteQuery(...)
inferData // InfiniteData
inferFnData // Data
inferVariables // Variables
inferError // Error
inferOptions // InfiniteQueryHookOptions<...>
```
## 禁用查询
要禁用查询,您可以将 `skipToken` 作为选项 `variables` 传递给您的自定义查询。这将阻止查询被执行。
```ts
import { skipToken } from '@tanstack/react-query'
const [name, setName] = useState()
const result = usePost({
variables: id ? { id: id } : skipToken,
})
// 以及用于 useQueries 的示例
const queries = useQueries({
queries: [usePost.getOptions(id ? { id: id } : skipToken)],
})
```
## 常见问题
### `getFetchOptions` 和 `getOptions` 有什么不同
`getFetchOptions` 只会返回必要的选项,而像 `staleTime` 和 `retry` 等选项会被忽略
### `fetcher` 和 `queryFn` 有什么不同
ReactQueryKit 会自动将 `fetcher` 转换为 `queryFn`,例如
```ts
const useTest = createQuery({
queryKey: ['test'],
fetcher: (variables, context) => {
// ...
},
})
// => useTest.getOptions(variables):
// {
// queryKey: ['test', variables],
// queryFn: (context) => fetcher(variables, context)
// }
```
## 迁移
从 ReactQueryKit 2 升级 → ReactQueryKit 3
```diff
createQuery({
- primaryKey: 'posts',
- queryFn: ({ queryKey: [_primaryKey, variables] }) => {},
+ queryKey: ['posts'],
+ fetcher: variables => {},
})
```
您可以从 ReactQueryKit 3 中受益
- 支持传入数组 `queryKey`
- 支持推断 fetcher 的类型,您可以自动享受首选的类型。
- 支持创建整个 API 的形状
## Issues
_Looking to contribute? Look for the [Good First Issue][good-first-issue]
label._
### 🐛 Bugs
请针对错误、缺少文档或意外行为提出问题。
[**See Bugs**][bugs]
### 💡 Feature Requests
请提交问题以建议新功能。 通过添加对功能请求进行投票
一个 👍。 这有助于维护人员优先处理要处理的内容。
[**See Feature Requests**][requests]
## LICENSE
MIT
[npm]: https://www.npmjs.com
[node]: https://nodejs.org
[bugs]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug
[requests]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement
[good-first-issue]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22
================================================
FILE: README.md
================================================
---
## What could you benefit from it
- Manage `queryKey` in a type-safe way
- Make `queryClient`'s operations clearly associated with custom ReactQuery hooks
- You can extract the TypeScript type of any custom ReactQuery hooks
- Middleware
English | [简体中文](./README-zh_CN.md)
## Table of Contents
- [Installation](#installation)
- [Examples](#examples)
- Usage
- [createQuery](#createquery)
- [createInfiniteQuery](#createinfinitequery)
- [createSuspenseQuery](#createsuspensequery)
- [createSuspenseInfiniteQuery](#createsuspenseinfinitequery)
- [createMutation](#createmutation)
- [router](#router)
- [Middleware](#middleware)
- [TypeScript](#typescript)
- [Type inference](#type-inference)
- [Disabling Queries](#disabling-queries)
- [FAQ](#faq)
- [Migration](#migration)
- [Issues](#issues)
- [🐛 Bugs](#-bugs)
- [💡 Feature Requests](#-feature-requests)
- [LICENSE](#license)
## Installation
This module is distributed via [npm][npm] which is bundled with [node][node] and
should be installed as one of your project's `dependencies`:
```bash
$ npm i react-query-kit
# or
$ yarn add react-query-kit
```
If you still on React Query Kit v2? Check out the v2 docs here: https://github.com/liaoliao666/react-query-kit/tree/v2#readme.
# Examples
- [Basic](https://codesandbox.io/s/example-react-query-kit-basic-1ny2j8)
- [Optimistic Updates](https://codesandbox.io/s/example-react-query-kit-optimistic-updates-eefg0v)
- [Next.js](https://codesandbox.io/s/example-react-query-kit-nextjs-uldl88)
- [Load-More & Infinite Scroll](https://codesandbox.io/s/example-react-query-kit-load-more-infinite-scroll-vg494v)
## createQuery
### Usage
```tsx
import { QueryClient, dehydrate } from '@tanstack/react-query'
import { createQuery } from 'react-query-kit'
type Data = { title: string; content: string }
type Variables = { id: number }
const usePost = createQuery({
queryKey: ['posts'],
fetcher: (variables: Variables): Promise => {
return fetch(`/posts/${variables.id}`).then(res => res.json())
},
// u can also pass middleware to cutomize this hook's behavior
use: [myMiddleware]
})
const variables = { id: 1 }
// example
export default function Page() {
// queryKey will be `['posts', { id: 1 }]` if u passed variables
const { data } = usePost({ variables })
return (
{data?.title}
{data?.content}
)
}
console.log(usePost.getKey()) // ['posts']
console.log(usePost.getKey(variables)) // ['posts', { id: 1 }]
// nextjs example
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(usePost.getFetchOptions(variables))
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
// usage outside of react component
const data = await queryClient.fetchQuery(usePost.getFetchOptions(variables))
// useQueries example
const queries = useQueries({
queries: [
usePost.getOptions(variables),
useUser.getOptions(),
],
})
// getQueryData
queryClient.getQueryData(usePost.getKey(variables)) // Data
// setQueryData
queryClient.setQueryData(usePost.getKey(variables), {...})
```
### Additional API Reference
Options
- `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise`
- Required
- The function that the query will use to request data. And The second param is the `QueryFunctionContext` of `queryFn`.
- `variables?: TVariables`
- Optional
- `variables` will be the frist param of fetcher and the last element of the `queryKey` array
- `use: Middleware[]`
- Optional
- array of middleware functions [(details)](#middleware)
Expose Methods
- `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise`
- `getKey: (variables: TVariables) => QueryKey`
- `getOptions: (variables: TVariables) => UseQueryOptions`
- `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn })`
## createInfiniteQuery
### Usage
```tsx
import { QueryClient, dehydrate } from '@tanstack/react-query'
import { createInfiniteQuery } from 'react-query-kit'
type Data = { projects: { id: string; name: string }[]; nextCursor: number }
type Variables = { active: boolean }
const useProjects = createInfiniteQuery({
queryKey: ['projects'],
fetcher: (variables: Variables, { pageParam }): Promise => {
return fetch(
`/projects?cursor=${pageParam}?active=${variables.active}`
).then(res => res.json())
},
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
initialPageParam: 0,
})
const variables = { active: true }
// example
export default function Page() {
// queryKey equals to ['projects', { active: true }]
const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } =
useProjects({ variables })
return (
{data.pages.map((group, i) => (
{group.projects.map(project => (
{project.name}
))}
))}
fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
{isFetching && !isFetchingNextPage ? 'Fetching...' : null}
)
}
// nextjs example
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchInfiniteQuery(
useProjects.getFetchOptions(variables)
)
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
// usage outside of react component
const data = await queryClient.fetchInfiniteQuery(
useProjects.getFetchOptions(variables)
)
```
### Additional API Reference
Options
- `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise`
- Required
- The function that the query will use to request data. And The second param is the `QueryFunctionContext` of `queryFn`.
- `variables?: TVariables`
- Optional
- `variables` will be the frist param of fetcher and the last element of the `queryKey` array
- `use: Middleware[]`
- Optional
- array of middleware functions [(details)](#middleware)
Expose Methods
- `fetcher: (variables: TVariables, context: QueryFunctionContext) => TFnData | Promise`
- `getKey: (variables: TVariables) => QueryKey`
- `getOptions: (variables: TVariables) => UseInfiniteQueryOptions`
- `getFetchOptions: (variables: TVariables) => ({ queryKey, queryFn, queryKeyHashFn, getNextPageParam, getPreviousPageParam, initialPageParam })`
## createSuspenseQuery
This has the same effect as setting the `suspense` option to `true` in the query config, but it works better in TypeScript, because `data` is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries).
```ts
import { createSuspenseQuery } from 'react-query-kit'
createSuspenseQuery({
...options,
})
// equals to
createQuery({
...options,
enabled: true,
suspense: true,
throwOnError: true,
})
```
## createSuspenseInfiniteQuery
```ts
import { createSuspenseInfiniteQuery } from 'react-query-kit'
createSuspenseInfiniteQuery({
...options,
})
// equals to
createInfiniteQuery({
...options,
enabled: true,
suspense: true,
throwOnError: true,
})
```
## createMutation
### Usage
```tsx
import { createMutation } from 'react-query-kit'
const useAddTodo = createMutation({
mutationFn: async (variables: { title: string; content: string }) =>
fetch('/post', {
method: 'POST',
body: JSON.stringify(variables),
}).then(res => res.json()),
onSuccess(data, variables, context) {
// do somethings
},
})
function App() {
const mutation = useAddTodo({
onSettled: (data, error, variables, context) => {
// Error or success... doesn't matter!
},
})
return (
{mutation.isPending ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
An error occurred: {mutation.error.message}
) : null}
{mutation.isSuccess ?
Todo added!
: null}
{
mutation.mutate({ title: 'Do Laundry', content: 'content...' })
}}
>
create Todo
>
)}
)
}
// usage outside of react component
useAddTodo.mutationFn({ title: 'Do Laundry', content: 'content...' })
```
### Additional API Reference
Options
- `use: Middleware[]`
- Optional
- array of middleware functions [(details)](#middleware)
Expose Methods
- `getKey: () => MutationKey`
- `getOptions: () => UseMutationOptions`
- `mutationFn: ExposeMutationFn`
## router
`router` which allow you to create a shape of your entire API
### Usage
```tsx
import { router } from 'react-query-kit'
const post = router(`post`, {
byId: router.query({
fetcher: (variables: { id: number }) =>
fetch(`/posts/${variables.id}`).then(res => res.json()),
use: [myMiddleware],
}),
list: router.infiniteQuery({
fetcher: (_variables, { pageParam }) =>
fetch(`/posts/?cursor=${pageParam}`).then(res => res.json()),
getNextPageParam: lastPage => lastPage.nextCursor,
initialPageParam: 0,
}),
add: router.mutation({
mutationFn: async (variables: { title: string; content: string }) =>
fetch('/posts', {
method: 'POST',
body: JSON.stringify(variables),
}).then(res => res.json()),
}),
// nest router
command: {
report: router.mutation({ mutationFn }),
promote: router.mutation({ mutationFn }),
},
})
// get root key
post.getKey() // ['post']
// hooks
post.byId.useQuery({ variables: { id: 1 } })
post.byId.useSuspenseQuery({ variables: { id: 1 } })
post.list.useInfiniteQuery()
post.list.useSuspenseInfiniteQuery()
post.add.useMutation()
post.command.report.useMutation()
// expose methods
post.byId.getKey({ id: 1 }) // ['post', 'byId', { id: 1 }]
post.byId.getFetchOptions({ id: 1 })
post.byId.getOptions({ id: 1 })
post.byId.fetcher({ id: 1 })
post.add.getKey() // ['post', 'add']
post.add.getOptions()
post.add.mutationFn({ title: 'title', content: 'content' })
// infer types
type Data = inferData
type FnData = inferFnData
type Variables = inferVariables
type Error = inferError
```
### Merging Routers
```ts
import { router } from 'react-query-kit'
const user = router(`user`, {})
const post = router(`post`, {})
const k = {
user,
post,
}
```
### API Reference
`type Router = (key: string | unknown[], config: TConfig) => TRouter`
Expose Methods
- `query`
Similar to `createQuery` but without option `queryKey`
- `infiniteQuery`
Similar to `createInfiniteQuery` but without option `queryKey`
- `mutation`
Similar to `createMutation` but without option `mutationKey`
## Middleware
This feature is inspired by the [Middleware feature from SWR](https://swr.vercel.app/docs/middleware). The middleware feature is a new addition in ReactQueryKit 1.5.0 that enables you to execute logic before and after hooks.
Middleware receive the hook and can execute logic before and after running it. If there are multiple middleware, each middleware wraps the next middleware. The last middleware in the list will receive the original hook.
### Usage
```ts
import { QueryClient } from '@tanstack/react-query'
import { Middleware, MutationHook, QueryHook, getKey } from 'react-query-kit'
const logger: Middleware> = useQueryNext => {
return options => {
const log = useLogger()
const fetcher = (variables, context) => {
log(context.queryKey, variables)
return options.fetcher(variables, context)
}
return useQueryNext({
...options,
fetcher,
})
}
}
const useUser = createQuery({
use: [logger],
})
// global middlewares
const queryMiddleware: Middleware = useQueryNext => {
return options => {
// u can also get queryKey via function getKey
const fullKey = getKey(options.queryKey, options.variables)
// ...
return useQueryNext(options)
}
}
const mutationMiddleware: Middleware = useMutationNext => {
return options => {
// ...
return useMutationNext(options)
}
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
use: [queryMiddleware],
},
mutations: {
use: [mutationMiddleware],
},
},
})
```
### Extend
Middleware will be merged from superior. For example:
```jsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
use: [a],
},
},
})
const useSomething = createQuery({
use: [b],
})
useSomething({ use: [c] })
```
is equivalent to:
```js
createQuery({ use: [a, b, c] })
```
### Multiple Middleware
Each middleware wraps the next middleware, and the last one just wraps the useQuery. For example:
```jsx
createQuery({ use: [a, b, c] })
```
The order of middleware executions will be a → b → c, as shown below:
```plaintext
enter a
enter b
enter c
useQuery()
exit c
exit b
exit a
```
### Multiple QueryClient
In ReactQuery v5, the `QueryClient` will be the second argument to `useQuery` and `useMutation`. If u have multiple `QueryClient` in global, u should receive `QueryClient` in middleware hook.
```ts
const useSomething = createQuery({
use: [
function myMiddleware(useQueryNext) {
// u should receive queryClient as the second argument here
return (options, queryClient) => {
const client = useQueryClient(queryClient)
// ...
return useQueryNext(options, queryClient)
}
},
],
})
// if u need to pass an another QueryClient
useSomething({...}, anotherQueryClient)
```
## TypeScript
By default, ReactQueryKit will also infer the types of `data` and `variables` from `fetcher`, so you can have the preferred types automatically.
```ts
type Data = { title: string; content: string }
type Variables = { id: number }
const usePost = createQuery({
queryKey: ['posts'],
fetcher: (variables: Variables): Promise => {
return fetch(`/posts/${variables}`).then(res => res.json())
},
})
// `data` will be inferred as `Data | undefined`.
// `variables` will be inferred as `Variables`.
const { data } = usePost({ variables: { id: 1 } })
```
You can also explicitly specify the types for `fetcher`‘s `variables` and `data`.
```ts
type Data = { title: string; content: string }
type Variables = { id: number }
const usePost = createQuery({
queryKey: ['posts'],
fetcher: variables => {
return fetch(`/posts/${variables}`).then(res => res.json())
},
})
// `data` will be inferred as `Data | undefined`.
// `error` will be inferred as `Error | null`
// `variables` will be inferred as `Variables`.
const { data, error } = usePost({ variables: { id: 1 } })
```
## Type inference
You can extract the TypeScript type of any custom hook with `inferData` or `inferVariables`
```ts
import { inferData, inferFnData, inferError, inferVariables, inferOptions } from 'react-query-kit'
const useProjects = createInfiniteQuery(...)
inferData // InfiniteData
inferFnData // Data
inferVariables // Variables
inferError // Error
inferOptions // InfiniteQueryHookOptions<...>
```
## Disabling Queries
To disable queries, you can pass `skipToken` as the option `variables` to your custom query. This will prevent the query from being executed.
```ts
import { skipToken } from '@tanstack/react-query'
const [name, setName] = useState()
const result = usePost({
variables: id ? { id: id } : skipToken,
})
// and for useQueries example
const queries = useQueries({
queries: [usePost.getOptions(id ? { id: id } : skipToken)],
})
```
## FAQ
### What is the difference between `getFetchOptions` and `getOptions`?
`getFetchOptions` would only return necessary options, while options like `staleTime` and `retry` would be omited
### What is the difference between `fetcher` and `queryFn`?
ReactQueryKit would automatically converts fetcher to queryFn, as shown below:
```ts
const useTest = createQuery({
queryKey: ['test'],
fetcher: (variables, context) => {
// ...
},
})
// => useTest.getOptions(variables):
// {
// queryKey: ['test', variables],
// queryFn: (context) => fetcher(variables, context)
// }
```
## Migration
Upgrading from ReactQueryKit 2 → ReactQueryKit 3
```diff
createQuery({
- primaryKey: 'posts',
- queryFn: ({ queryKey: [_primaryKey, variables] }) => {},
+ queryKey: ['posts'],
+ fetcher: variables => {},
})
```
What you benefit from ReactQueryKit 3
- Support hierarchical key
- Support infer the types of fetcher, you can enjoy the preferred types automatically.
- Support to create a shape of your entire API
## Issues
_Looking to contribute? Look for the [Good First Issue][good-first-issue]
label._
### 🐛 Bugs
Please file an issue for bugs, missing documentation, or unexpected behavior.
[**See Bugs**][bugs]
### 💡 Feature Requests
Please file an issue to suggest new features. Vote on feature requests by adding
a 👍. This helps maintainers prioritize what to work on.
[**See Feature Requests**][requests]
## LICENSE
MIT
[npm]: https://www.npmjs.com
[node]: https://nodejs.org
[bugs]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug
[requests]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement
[good-first-issue]: https://github.com/liaoliao666/react-query-kit/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22
================================================
FILE: babel.config.js
================================================
module.exports = {
presets: [
[
'@babel/preset-env',
{
loose: true,
modules: false,
exclude: [
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-parameters',
],
},
],
'@babel/preset-typescript',
],
}
================================================
FILE: jest.config.js
================================================
/** @type {import('jest').Config} */
const config = {
transform: {
'\\.[jt]sx?$': 'ts-jest',
},
testEnvironment: 'jsdom',
}
module.exports = config
================================================
FILE: package.json
================================================
{
"name": "react-query-kit",
"version": "3.3.3",
"description": "🕊️ A toolkit for ReactQuery that make ReactQuery hooks more reusable and typesafe",
"author": "liaoliao666",
"repository": "liaoliao666/react-query-kit",
"homepage": "https://github.com/liaoliao666/react-query-kit#readme",
"types": "build/lib/index.d.ts",
"main": "build/lib/index.js",
"module": "build/lib/index.esm.js",
"exports": {
".": {
"types": "./build/lib/index.d.ts",
"import": "./build/lib/index.mjs",
"default": "./build/lib/index.js"
},
"./package.json": "./package.json"
},
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@babel/preset-typescript": "^7.18.6",
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-node-resolve": "^13.2.1",
"@rollup/plugin-replace": "^4.0.0",
"@tanstack/react-query": "^5.100.6",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/jest": "^28.1.6",
"@typescript-eslint/eslint-plugin": "^5.32.0",
"@typescript-eslint/parser": "^5.32.0",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^26.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.1",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"prettier": "^2.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"replace": "^1.2.1",
"rollup": "^2.77.2",
"rollup-plugin-size": "^0.2.2",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-visualizer": "^5.7.1",
"ts-jest": "^29.1.0",
"typescript": "^5.1.6"
},
"peerDependencies": {
"@tanstack/react-query": "^4 || ^5"
},
"peerDependenciesMeta": {
"@tanstack/react-query": {
"optional": true
}
},
"sideEffects": false,
"scripts": {
"build": "rollup --config rollup.config.js && npm run typecheck",
"typecheck": "tsc -b && tsc -p tsconfig.types.json --pretty false",
"stats": "open ./build/stats-html.html",
"eslint": "eslint --fix '*.{js,json}' '{src,tests,benchmarks}/**/*.{ts,tsx}'",
"test": "jest"
},
"dependencies": {},
"files": [
"build/*",
"src"
],
"keywords": [
"react",
"react-query"
]
}
================================================
FILE: prettier.config.js
================================================
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
trailingComma: 'es5',
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: 'avoid',
endOfLine: 'auto',
plugins: [require('@trivago/prettier-plugin-sort-imports')],
importOrder: ['^[./]'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
}
================================================
FILE: rollup.config.js
================================================
import { babel } from '@rollup/plugin-babel'
import commonJS from '@rollup/plugin-commonjs'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import size from 'rollup-plugin-size'
import { terser } from 'rollup-plugin-terser'
import visualizer from 'rollup-plugin-visualizer'
const replaceDevPlugin = type =>
replace({
'process.env.NODE_ENV': `"${type}"`,
delimiters: ['', ''],
preventAssignment: true,
})
const extensions = ['.ts', '.tsx']
const babelPlugin = babel({
babelHelpers: 'bundled',
exclude: /node_modules/,
extensions,
})
export default function rollup() {
const options = {
input: 'src/index.ts',
jsName: 'ReactQueryKit',
external: ['@tanstack/react-query'],
globals: {
'@tanstack/react-query': 'ReactQuery',
},
}
return [
mjs(options),
esm(options),
cjs(options),
umdDev(options),
umdProd(options),
]
}
function mjs({ input, external }) {
return {
// ESM
external,
input,
output: {
format: 'esm',
sourcemap: true,
dir: `build/lib`,
preserveModules: true,
entryFileNames: '[name].mjs',
},
plugins: [babelPlugin, commonJS(), nodeResolve({ extensions })],
}
}
function esm({ input, external }) {
return {
// ESM
external,
input,
output: {
format: 'esm',
dir: `build/lib`,
sourcemap: true,
preserveModules: true,
entryFileNames: '[name].esm.js',
},
plugins: [babelPlugin, commonJS(), nodeResolve({ extensions })],
}
}
function cjs({ input, external }) {
return {
// CJS
external,
input,
output: {
format: 'cjs',
sourcemap: true,
dir: `build/lib`,
preserveModules: true,
exports: 'named',
entryFileNames: '[name].js',
},
plugins: [babelPlugin, commonJS(), nodeResolve({ extensions })],
}
}
function umdDev({ input, external, globals, jsName }) {
return {
// UMD (Dev)
external,
input,
output: {
format: 'umd',
sourcemap: true,
file: `build/umd/index.development.js`,
name: jsName,
globals,
},
plugins: [
babelPlugin,
commonJS(),
nodeResolve({ extensions }),
replaceDevPlugin('development'),
],
}
}
function umdProd({ input, external, globals, jsName }) {
return {
// UMD (Prod)
external,
input,
output: {
format: 'umd',
sourcemap: true,
file: `build/umd/index.production.js`,
name: jsName,
globals,
},
plugins: [
babelPlugin,
commonJS(),
nodeResolve({ extensions }),
replaceDevPlugin('production'),
terser({
mangle: true,
compress: true,
}),
size({}),
visualizer({
filename: `build/stats-html.html`,
gzipSize: true,
}),
visualizer({
filename: `build/stats.json`,
json: true,
gzipSize: true,
}),
],
}
}
================================================
FILE: src/createBaseQuery.ts
================================================
import {
type QueryClient,
type QueryFunctionContext,
type UseBaseQueryOptions,
type UseInfiniteQueryOptions,
} from '@tanstack/react-query'
import { ReactQuery, getKey as getFullKey, withMiddleware } from './utils'
type QueryBaseHookOptions = Omit<
UseBaseQueryOptions,
'queryKey' | 'queryFn'
> & {
fetcher?: any
variables?: any
}
export const createBaseQuery = (
defaultOptions: any,
useRQHook: (options: any, queryClient?: any) => any,
overrideOptions?: Partial
): any => {
if (process.env.NODE_ENV !== 'production') {
// @ts-ignore
if (defaultOptions.useDefaultOptions) {
console.error(
'[Bug] useDefaultOptions is not supported, please use middleware instead.'
)
}
// @ts-ignore
if (defaultOptions.queryFn) {
console.error(
'[Bug] queryFn is not supported, please use fetcher instead.'
)
}
}
const getQueryOptions = (fetcherFn: any, variables: any) => {
return {
queryFn:
variables && variables === ReactQuery.skipToken
? ReactQuery.skipToken
: (context: QueryFunctionContext) => fetcherFn(variables, context),
queryKey: getFullKey(defaultOptions.queryKey, variables),
}
}
const getKey = (variables?: any) =>
getFullKey(defaultOptions.queryKey, variables)
const getOptions = (variables: any) => {
return {
...defaultOptions,
...getQueryOptions(defaultOptions.fetcher, variables),
}
}
const getFetchOptions = (variables: any) => {
return {
...getQueryOptions(defaultOptions.fetcher, variables),
queryKeyHashFn: defaultOptions.queryKeyHashFn,
getPreviousPageParam: defaultOptions.getPreviousPageParam,
getNextPageParam: defaultOptions.getNextPageParam,
initialPageParam: defaultOptions.initialPageParam,
}
}
const useBaseHook = (
options: QueryBaseHookOptions,
queryClient?: QueryClient
) => {
return useRQHook(
{
...options,
...getQueryOptions(options.fetcher, options.variables),
...overrideOptions,
},
queryClient
)
}
return Object.assign(withMiddleware(useBaseHook, defaultOptions, 'queries'), {
fetcher: defaultOptions.fetcher,
getKey,
getOptions,
getFetchOptions,
})
}
================================================
FILE: src/createInfiniteQuery.ts
================================================
import { createBaseQuery } from './createBaseQuery'
import type {
CompatibleError,
CreateInfiniteQueryOptions,
InfiniteQueryHook,
} from './types'
import { ReactQuery } from './utils'
export function createInfiniteQuery<
TFnData,
TVariables = void,
TError = CompatibleError,
TPageParam = number
>(
options: CreateInfiniteQueryOptions
): InfiniteQueryHook {
return createBaseQuery(options, ReactQuery.useInfiniteQuery)
}
================================================
FILE: src/createMutation.ts
================================================
import type {
CompatibleError,
CreateMutationOptions,
MutationHook,
} from './types'
import { ReactQuery, withMiddleware } from './utils'
export function createMutation<
TData = unknown,
TVariables = void,
TError = CompatibleError,
TContext = unknown
>(
defaultOptions: CreateMutationOptions
): MutationHook {
return Object.assign(
withMiddleware(ReactQuery.useMutation, defaultOptions, 'mutations'),
{
getKey: () => defaultOptions.mutationKey,
getOptions: () => defaultOptions,
mutationFn: defaultOptions.mutationFn,
}
) as MutationHook
}
================================================
FILE: src/createQuery.ts
================================================
import { createBaseQuery } from './createBaseQuery'
import type { CompatibleError, CreateQueryOptions, QueryHook } from './types'
import { ReactQuery } from './utils'
export function createQuery<
TFnData,
TVariables = void,
TError = CompatibleError
>(
options: CreateQueryOptions
): QueryHook {
return createBaseQuery(options, ReactQuery.useQuery)
}
================================================
FILE: src/createSuspenseInfiniteQuery.ts
================================================
import { createBaseQuery } from './createBaseQuery'
import type {
CompatibleError,
CreateSuspenseInfiniteQueryOptions,
SuspenseInfiniteQueryHook,
} from './types'
import { ReactQuery, isV5, suspenseOptions } from './utils'
export function createSuspenseInfiniteQuery<
TFnData,
TVariables = void,
TError = CompatibleError,
TPageParam = number
>(
options: CreateSuspenseInfiniteQueryOptions<
TFnData,
TVariables,
TError,
TPageParam
>
): SuspenseInfiniteQueryHook {
return isV5
? createBaseQuery(options, ReactQuery.useSuspenseInfiniteQuery)
: createBaseQuery(options, ReactQuery.useInfiniteQuery, suspenseOptions)
}
================================================
FILE: src/createSuspenseQuery.ts
================================================
import { createBaseQuery } from './createBaseQuery'
import type {
CompatibleError,
CreateSuspenseQueryOptions,
SuspenseQueryHook,
} from './types'
import { ReactQuery, isV5, suspenseOptions } from './utils'
export function createSuspenseQuery<
TFnData,
TVariables = void,
TError = CompatibleError
>(
options: CreateSuspenseQueryOptions
): SuspenseQueryHook {
return isV5
? createBaseQuery(options, ReactQuery.useSuspenseQuery)
: createBaseQuery(options, ReactQuery.useQuery, suspenseOptions)
}
================================================
FILE: src/index.ts
================================================
export * from './createQuery'
export * from './createSuspenseQuery'
export * from './createInfiniteQuery'
export * from './createSuspenseInfiniteQuery'
export * from './createMutation'
export * from './types'
export * from './router'
export { getKey } from './utils'
================================================
FILE: src/router.ts
================================================
import { QueryKey } from '@tanstack/react-query'
import { createInfiniteQuery } from './createInfiniteQuery'
import { createMutation } from './createMutation'
import { createQuery } from './createQuery'
import { createSuspenseInfiniteQuery } from './createSuspenseInfiniteQuery'
import { createSuspenseQuery } from './createSuspenseQuery'
import type {
CompatibleError,
CreateRouter,
RouterConfig,
RouterInfiniteQuery,
RouterInfiniteQueryOptions,
RouterMutation,
RouterMutationOptions,
RouterQuery,
RouterQueryOptions,
} from './types'
type RouterNode = RouterConfig[string]
type RouterTypedNode = RouterNode & {
_routerType: 'q' | 'inf' | 'm'
}
const isRouterLeaf = (value: RouterNode): value is RouterTypedNode => {
return !!value && typeof value._routerType === 'string'
}
const buildRouter = (keys: QueryKey, config: RouterConfig) => {
return Object.entries(config).reduce(
(acc, [key, opts]) => {
if (!isRouterLeaf(opts)) {
acc[key] = buildRouter([...keys, key], opts)
} else {
const type = opts._routerType
const options: any = {
...opts,
[type === `m` ? `mutationKey` : `queryKey`]: [...keys, key],
}
if (type === `m`) {
acc[key] = {
useMutation: createMutation(options),
...createMutation(options),
}
return acc
}
acc[key] =
type === `q`
? {
useQuery: createQuery(options),
useSuspenseQuery: createSuspenseQuery(options),
...createQuery(options),
}
: {
useInfiniteQuery: createInfiniteQuery(options),
useSuspenseInfiniteQuery: createSuspenseInfiniteQuery(options),
...createInfiniteQuery(options),
}
}
return acc
},
{
getKey: () => keys,
} as any
)
}
export const router = (
key: string | QueryKey,
config: TConfig
): CreateRouter => {
return buildRouter(Array.isArray(key) ? key : [key], config)
}
function query(
options: RouterQueryOptions
): RouterQuery {
return {
...options,
_routerType: 'q',
}
}
function infiniteQuery<
TFnData,
TVariables = void,
TError = CompatibleError,
TPageParam = number
>(
options: RouterInfiniteQueryOptions
): RouterInfiniteQuery {
return { ...options, _routerType: 'inf' } as RouterInfiniteQuery<
TFnData,
TVariables,
TError,
TPageParam
>
}
function mutation<
TFnData = unknown,
TVariables = void,
TError = CompatibleError,
TContext = unknown
>(
options: RouterMutationOptions
): RouterMutation {
return { ...options, _routerType: 'm' } as RouterMutation<
TFnData,
TVariables,
TError,
TContext
>
}
router.query = query
router.infiniteQuery = infiniteQuery
router.mutation = mutation
================================================
FILE: src/types.ts
================================================
import type {
DataTag,
DefaultError,
DefinedUseInfiniteQueryResult,
DefinedUseQueryResult,
InfiniteData,
InfiniteQueryObserverSuccessResult,
MutationFunctionContext,
MutationKey,
QueryClient,
QueryFunction,
QueryFunctionContext,
QueryKey,
QueryKeyHashFunction,
QueryObserverSuccessResult,
SkipToken,
UseInfiniteQueryOptions,
UseInfiniteQueryResult,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from '@tanstack/react-query'
// utils
type CompatibleWithV4 =
InfiniteData extends UseInfiniteQueryResult<
InfiniteData
>['data']
? V5
: V4
type CompatibleWithTwoV5 = UseInfiniteQueryOptions<
unknown,
DefaultError,
unknown,
QueryKey,
unknown
> extends never
? V5Generics
: V6Generics
type CompatibleUseInfiniteQueryOptions =
CompatibleWithV4<
CompatibleWithTwoV5<
UseInfiniteQueryOptions,
// @ts-ignore
UseInfiniteQueryOptions<
TFnData,
TError,
TData,
TFnData,
QueryKey,
TPageParam
>
>,
// @ts-ignore
UseInfiniteQueryOptions
>
type CompatibleInfiniteData = CompatibleWithV4<
InfiniteData,
InfiniteData
>
type NonUndefinedGuard = T extends undefined ? never : T
type WithRequired = T & {
[_ in K]: {}
}
type DeepPartial = T extends object
? {
[P in keyof T]?: DeepPartial
}
: T
type DefaultTo = unknown extends T ? D : T
export type CompatibleError = CompatibleWithV4
export type Fetcher = (
variables: TVariables,
context: QueryFunctionContext
) => TFnData | Promise
export type AdditionalQueryOptions = {
fetcher: Fetcher
variables?: TVariables
}
type inferMiddlewareHook any> = (
options: inferCreateOptions,
queryClient?: CompatibleWithV4
) => ReturnType
export type Middleware<
T extends (...args: any) => any = QueryHook
> = (hook: inferMiddlewareHook) => inferMiddlewareHook
export type ExposeFetcher = (
variables: TVariables,
context?: Partial>
) => TFnData | Promise
export type ExposeMethods = {
fetcher: ExposeFetcher
getKey: (
variables?: DeepPartial
) => CompatibleWithV4<
DataTag<
QueryKey,
[TPageParam] extends [never] ? TFnData : InfiniteData
>,
QueryKey
>
getFetchOptions: (
variables: TVariables extends void
? CompatibleWithV4 | void
: CompatibleWithV4
) => Pick<
ReturnType<
ExposeMethods['getOptions']
>,
// @ts-ignore
[TPageParam] extends [never]
? 'queryKey' | 'queryFn' | 'queryKeyHashFn'
:
| 'queryKey'
| 'queryFn'
| 'queryKeyHashFn'
| 'getNextPageParam'
| 'getPreviousPageParam'
| 'initialPageParam'
>
getOptions: (
variables: TVariables extends void
? CompatibleWithV4 | void
: CompatibleWithV4
) => [TPageParam] extends [never]
? CompatibleWithV4<
UseQueryOptions & {
queryKey: DataTag
queryFn?: Exclude<
UseQueryOptions['queryFn'],
SkipToken
>
},
// Not work to infer TError in v4
{
queryKey: QueryKey
queryFn: QueryFunction
queryKeyHashFn?: QueryKeyHashFunction
}
>
: CompatibleUseInfiniteQueryOptions<
TFnData,
TFnData,
TError,
TPageParam
> & {
queryKey: CompatibleWithV4<
DataTag>,
QueryKey
>
}
}
type Clone = T extends infer TClone ? TClone : never
// query hook
export interface CreateQueryOptions<
TFnData = unknown,
TVariables = void,
TError = CompatibleError
> extends Omit<
UseQueryOptions,
'queryKey' | 'queryFn' | 'select'
>,
AdditionalQueryOptions {
queryKey: QueryKey
use?: Middleware<
QueryHook, Clone, Clone>
>[]
variables?: TVariables
}
export interface QueryHookOptions
extends Omit<
UseQueryOptions,
'queryKey' | 'queryFn' | 'queryKeyHashFn'
> {
use?: Middleware>[]
variables?: CompatibleWithV4
}
export interface DefinedQueryHookOptions
extends Omit<
QueryHookOptions,
'initialData'
> {
initialData: NonUndefinedGuard | (() => NonUndefinedGuard)
}
export type QueryHookResult = UseQueryResult
export type DefinedQueryHookResult = DefinedUseQueryResult<
TData,
TError
>
export interface QueryHook<
TFnData = unknown,
TVariables = void,
TError = CompatibleError
> extends ExposeMethods {
(
options: DefinedQueryHookOptions,
queryClient?: CompatibleWithV4
): DefinedQueryHookResult
(
options?: QueryHookOptions,
queryClient?: CompatibleWithV4
): QueryHookResult
}
// suspense query hook
export interface CreateSuspenseQueryOptions<
TFnData = unknown,
TVariables = void,
TError = CompatibleError
> extends Omit<
UseQueryOptions,
| 'queryKey'
| 'queryFn'
| 'enabled'
| 'select'
| 'suspense'
| 'throwOnError'
| 'placeholderData'
| 'keepPreviousData'
| 'useErrorBoundary'
>,
AdditionalQueryOptions {
queryKey: QueryKey
use?: Middleware<
SuspenseQueryHook, Clone, Clone>
>[]
variables?: TVariables
}
export interface SuspenseQueryHookOptions
extends Omit<
UseQueryOptions,
| 'queryKey'
| 'queryFn'
| 'queryKeyHashFn'
| 'enabled'
| 'suspense'
| 'throwOnError'
| 'placeholderData'
| 'keepPreviousData'
| 'useErrorBoundary'
> {
use?: Middleware>[]
variables?: CompatibleWithV4
}
export type SuspenseQueryHookResult = Omit<
QueryObserverSuccessResult,
'isPlaceholderData' | 'isPreviousData'
>
export interface SuspenseQueryHook<
TFnData = unknown,
TVariables = void,
TError = CompatibleError
> extends ExposeMethods {
(
options?: SuspenseQueryHookOptions,
queryClient?: CompatibleWithV4
): SuspenseQueryHookResult
}
// infinite query hook
export interface CreateInfiniteQueryOptions<
TFnData = unknown,
TVariables = void,
TError = CompatibleError,
TPageParam = number
> extends Omit<
CompatibleUseInfiniteQueryOptions,
'queryKey' | 'queryFn' | 'select'
>,
AdditionalQueryOptions {
queryKey: QueryKey
use?: Middleware<
InfiniteQueryHook<
Clone,
Clone,
Clone,
Clone
>
>[]
variables?: TVariables
}
export interface InfiniteQueryHookOptions<
TFnData,
TError,
TData,
TVariables,
TPageParam = number
> extends Omit<
CompatibleUseInfiniteQueryOptions,
| 'queryKey'
| 'queryFn'
| 'queryKeyHashFn'
| 'initialPageParam'
| 'getPreviousPageParam'
| 'getNextPageParam'
> {
use?: Middleware>[]
variables?: CompatibleWithV4
}
export interface DefinedInfiniteQueryHookOptions<
TFnData,
TError,
TData,
TVariables,
TPageParam = number
> extends Omit<
InfiniteQueryHookOptions,
'initialData'
> {
initialData:
| NonUndefinedGuard>
| (() => NonUndefinedGuard>)
}
export type InfiniteQueryHookResult = UseInfiniteQueryResult<
TData,
TError
>
export type DefinedInfiniteQueryHookResult = CompatibleWithV4<
DefinedUseInfiniteQueryResult,
WithRequired, 'data'>
>
export interface InfiniteQueryHook<
TFnData = unknown,
TVariables = void,
TError = CompatibleError,
TPageParam = number
> extends ExposeMethods {
, TFnData>>(
options: DefinedInfiniteQueryHookOptions<
TFnData,
TError,
TData,
TVariables,
TPageParam
>,
queryClient?: CompatibleWithV4
): DefinedInfiniteQueryHookResult
, TFnData>>(
options?: InfiniteQueryHookOptions<
TFnData,
TError,
TData,
TVariables,
TPageParam
>,
queryClient?: CompatibleWithV4
): InfiniteQueryHookResult
}
// infinite sususpense query hook
export interface CreateSuspenseInfiniteQueryOptions<
TFnData = unknown,
TVariables = void,
TError = CompatibleError,
TPageParam = number
> extends Omit<
CompatibleUseInfiniteQueryOptions,
| 'queryKey'
| 'queryFn'
| 'enabled'
| 'select'
| 'suspense'
| 'throwOnError'
| 'placeholderData'
| 'keepPreviousData'
| 'useErrorBoundary'
>,
AdditionalQueryOptions {
queryKey: QueryKey
use?: Middleware<
SuspenseInfiniteQueryHook<
Clone,
Clone,
Clone,
Clone
>
>[]
variables?: TVariables
}
export interface SuspenseInfiniteQueryHookOptions<
TFnData,
TError,
TData,
TVariables,
TPageParam = number
> extends Omit<
CompatibleUseInfiniteQueryOptions,
| 'queryKey'
| 'queryFn'
| 'queryKeyHashFn'
| 'enabled'
| 'initialPageParam'
| 'getPreviousPageParam'
| 'getNextPageParam'
| 'suspense'
| 'throwOnError'
| 'placeholderData'
| 'keepPreviousData'
| 'useErrorBoundary'
> {
use?: Middleware>[]
variables?: CompatibleWithV4
}
export type SuspenseInfiniteQueryHookResult = Omit<
InfiniteQueryObserverSuccessResult,
'isPlaceholderData' | 'isPreviousData'
>
export interface SuspenseInfiniteQueryHook<
TFnData = unknown,
TVariables = void,
TError = CompatibleError,
TPageParam = number
> extends ExposeMethods {
, TFnData>>(
options?: SuspenseInfiniteQueryHookOptions<
TFnData,
TError,
TData,
TVariables,
TPageParam
>,
queryClient?: CompatibleWithV4
): SuspenseInfiniteQueryHookResult
}
// mutation hook
export interface CreateMutationOptions<
TData = unknown,
TVariables = void,
TError = CompatibleError,
TContext = unknown
> extends UseMutationOptions {
use?: Middleware>[]
}
export interface MutationHookOptions
extends Omit<
UseMutationOptions,
'mutationFn' | 'mutationKey'
> {
use?: Middleware>[]
}
export type MutationHookResult<
TData = unknown,
TError = CompatibleError,
TVariables = void,
TContext = unknown
> = UseMutationResult
export type ExposeMutationFn = [
TVariables
] extends [void]
? (
variables?: TVariables,
context?: Partial
) => Promise
: (
variables: TVariables,
context?: Partial
) => Promise
export interface ExposeMutationMethods<
TData = unknown,
TVariables = void,
TError = CompatibleError,
TDefaultContext = unknown
> {
getKey: () => MutationKey | undefined
getOptions: () => UseMutationOptions<
TData,
TError,
TVariables,
TDefaultContext
>
mutationFn: ExposeMutationFn
}
export interface MutationHook<
TData = unknown,
TVariables = void,
TError = CompatibleError,
TDefaultContext = unknown
> extends ExposeMutationMethods {
(
options?: MutationHookOptions,
queryClient?: CompatibleWithV4
): MutationHookResult
}
// infer types
export type inferVariables = T extends {
fetcher: ExposeFetcher
}
? TVariables
: T extends ExposeMutationMethods
? TVariables
: never
export type inferData = T extends {
fetcher: ExposeFetcher
}
? [TPageParam] extends [never]
? TFnData
: CompatibleInfiniteData
: T extends ExposeMutationMethods
? TFnData
: never
export type inferFnData = T extends {
fetcher: ExposeFetcher
}
? TFnData
: T extends ExposeMutationMethods
? TFnData
: never
export type inferError = T extends ExposeMethods
? TError
: T extends ExposeMethods
? TError
: T extends ExposeMutationMethods
? TError
: never
export type inferOptions = T extends QueryHook<
infer TFnData,
infer TVariables,
infer TError
>
? QueryHookOptions
: T extends SuspenseQueryHook
? SuspenseQueryHookOptions
: T extends InfiniteQueryHook<
infer TFnData,
infer TVariables,
infer TError,
infer TPageParam
>
? InfiniteQueryHookOptions<
TFnData,
TError,
CompatibleWithV4, TFnData>,
TVariables,
TPageParam
>
: T extends SuspenseInfiniteQueryHook<
infer TFnData,
infer TVariables,
infer TError,
infer TPageParam
>
? SuspenseInfiniteQueryHookOptions<
TFnData,
TError,
CompatibleWithV4, TFnData>,
TVariables,
TPageParam
>
: T extends MutationHook
? MutationHookOptions
: never
export type inferCreateOptions = T extends QueryHook<
infer TFnData,
infer TVariables,
infer TError
>
? CreateQueryOptions
: T extends SuspenseQueryHook
? CreateSuspenseQueryOptions
: T extends InfiniteQueryHook<
infer TFnData,
infer TVariables,
infer TError,
infer TPageParam
>
? CreateInfiniteQueryOptions
: T extends SuspenseInfiniteQueryHook<
infer TFnData,
infer TVariables,
infer TError,
infer TPageParam
>
? CreateSuspenseInfiniteQueryOptions
: T extends MutationHook<
infer TFnData,
infer TVariables,
infer TError,
infer TContext
>
? CreateMutationOptions
: never
// router
export type RouterQueryOptions<
TFnData,
TVariables = void,
TError = CompatibleError
> = Omit, 'queryKey'>
export type RouterQuery<
TFnData,
TVariables = void,
TError = CompatibleError
> = RouterQueryOptions & {
_routerType: `q`
}
export type ResolvedRouterQuery<
TFnData,
TVariables = void,
TError = CompatibleError
> = {
useQuery: QueryHook
useSuspenseQuery: SuspenseQueryHook