Full Code of liaoliao666/react-query-kit for AI

main a00020f8ca8c cached
35 files
94.4 KB
26.8k tokens
62 symbols
1 requests
Download .txt
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
================================================
<div align="center">

<br />
<br />

<p align="center">
  <a aria-label="NPM version" href="./assets/logo.svg">
    <img alt="" src="./assets/logo.svg" height="40">
  </a>
</p>

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

<p align="center">
  <a href="https://github.com/liaoliao666/react-query-kit/actions/workflows/tests.yml"><img src="https://github.com/liaoliao666/react-query-kit/actions/workflows/tests.yml/badge.svg?branch=main" alt="Latest build" target="\_parent"></a>
  <a href="https://www.npmjs.com/package/react-query-kit"><img src="https://badgen.net/npm/v/react-query-kit" alt="Latest published version" target="\_parent"></a>
  <a href="https://github.com/liaoliao666/react-query-kit"><img src="https://badgen.net/npm/types/react-query-kit" alt="Types included" target="\_parent"></a>
  <a href="https://www.npmjs.com/package/react-query-kit"><img src="https://badgen.net/npm/license/react-query-kit" alt="License" target="\_parent"></a>
  <a href="https://www.npmjs.com/package/react-query-kit"><img src="https://badgen.net/npm/dt/react-query-kit" alt="Number of downloads" target="\_parent"></a>
  <a href="https://github.com/liaoliao666/react-query-kit"><img src="https://img.shields.io/github/stars/liaoliao666/react-query-kit.svg?style=social&amp;label=Star" alt="GitHub Stars" target="\_parent"></a>
</p>
</div>

---

## Motivation

- 以类型安全的方式管理 `queryKey`
- 让 `queryClient` 的操作更清楚地关联到哪个自定义 hook
- 可以从任何自定义 ReactQuery hook 中提取的 TypeScript 类型
- 中间件

[English](./README.md) | 简体中文

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [安装](#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)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## 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<Data> => {
    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 (
    <div>
      <div>{data?.title}</div>
      <div>{data?.content}</div>
    </div>
  )
}

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<QueryKey, TPageParam>) => TFnData | Promise<TFnData>`
  - 必填
  - 用于请求数据的函数。 第二个参数是“queryFn”的“QueryFunctionContext”
- `variables?: TVariables`
  - 可选
  - `variables` 将是 fetcher 的第一个参数和 `queryKey` 数组的最后一个元素
- `use: Middleware[]`
  - 可选
  - 中间件函数数组 [(详情)](#中间件)

Expose Methods

- `fetcher: (variables: TVariables, context: QueryFunctionContext<QueryKey, TPageParam>) => TFnData | Promise<TFnData>`
- `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<Data> => {
    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 (
    <div>
      {data.pages.map((group, i) => (
        <React.Fragment key={i}>
          {group.projects.map(project => (
            <p key={project.id}>{project.name}</p>
          ))}
        </React.Fragment>
      ))}
      <div>
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetchingNextPage}
        >
          {isFetchingNextPage
            ? 'Loading more...'
            : hasNextPage
            ? 'Load More'
            : 'Nothing more to load'}
        </button>
      </div>
      <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
    </div>
  )
}

// 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<QueryKey, TPageParam>) => TFnData | Promise<TFnData>`
  - 必填
  - 查询将用于请求数据的函数。 第二个参数是“queryFn”的“QueryFunctionContext”
- `variables?: TVariables`
  - 可选
  - `variables` 将是 fetcher 的第一个参数和 `queryKey` 数组的最后一个元素
- `use: Middleware[]`
  - 可选
  - 中间件函数数组 [(详情)](#中间件)

Expose Methods

- `fetcher: (variables: TVariables, context: QueryFunctionContext<QueryKey, TPageParam>) => TFnData | Promise<TFnData>`
- `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 (
    <div>
      {mutation.isPending ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}

          {mutation.isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutation.mutate({ title: 'Do Laundry', content: 'content...' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

// usage outside of react component
useAddTodo.mutationFn({ title: 'Do Laundry', content: 'content...' })
```

### 额外的 API 文档

Options

- `use: Middleware[]`
  - 可选
  - 中间件函数数组 [(详情)](#中间件)

Expose Methods

- `getKey: () => MutationKey`
- `getOptions: () => UseMutationOptions`
- `mutationFn: ExposeMutationFn<TData, TVariables>`

## 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<typeof post.list>
type FnData = inferFnData<typeof post.list>
type Variables = inferVariables<typeof post.list>
type Error = inferError<typeof post.list>
```

### 合并路由

```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<QueryHook<Data, Variables>> = 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<Data, Variables>({
  use: [logger],
})

// 全局中间件
const queryMiddleware: Middleware<QueryHook> = useQueryNext => {
  return options => {
    // 你还可以通过函数 getKey 获取 queryKey
    const fullKey = getKey(options.queryKey, options.variables)
    // ...
    return useQueryNext(options)
  }
}
const mutationMiddleware: Middleware<MutationHook> = 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<Data> => {
    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<Data, Variables, Error>({
  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<Data, Variables>(...)

inferData<typeof useProjects> // InfiniteData<Data>
inferFnData<typeof useProjects> // Data
inferVariables<typeof useProjects> // Variables
inferError<typeof useProjects> // Error
inferOptions<typeof useProjects> // InfiniteQueryHookOptions<...>
```

## 禁用查询

要禁用查询,您可以将 `skipToken` 作为选项 `variables` 传递给您的自定义查询。这将阻止查询被执行。

```ts
import { skipToken } from '@tanstack/react-query'

const [name, setName] = useState<string | undefined>()
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

<!-- prettier-ignore-start -->
[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
<!-- prettier-ignore-end -->


================================================
FILE: README.md
================================================
<div align="center">

<br />
<br />

<p align="center">
  <a aria-label="NPM version" href="./assets/logo.svg">
    <img alt="" src="./assets/logo.svg" height="40">
  </a>
</p>

<p>🕊️ A toolkit for ReactQuery that make ReactQuery reusable and typesafe</p>

<p align="center">
  <a href="https://github.com/liaoliao666/react-query-kit/actions/workflows/tests.yml"><img src="https://github.com/liaoliao666/react-query-kit/actions/workflows/tests.yml/badge.svg?branch=main" alt="Latest build" target="\_parent"></a>
  <a href="https://www.npmjs.com/package/react-query-kit"><img src="https://badgen.net/npm/v/react-query-kit" alt="Latest published version" target="\_parent"></a>
  <a href="https://github.com/liaoliao666/react-query-kit"><img src="https://badgen.net/npm/types/react-query-kit" alt="Types included" target="\_parent"></a>
  <a href="https://github.com/liaoliao666/react-query-kit/blob/main/LICENSE"><img src="https://badgen.net/npm/license/react-query-kit" alt="License" target="\_parent"></a>
  <a href="https://www.npmjs.com/package/react-query-kit"><img src="https://badgen.net/npm/dt/react-query-kit" alt="Number of downloads" target="\_parent"></a>
  <a href="https://github.com/liaoliao666/react-query-kit"><img src="https://img.shields.io/github/stars/liaoliao666/react-query-kit.svg?style=social&amp;label=Star" alt="GitHub Stars" target="\_parent"></a>
</p>
</div>

---

## 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

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [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)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## 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<Data> => {
    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 (
    <div>
      <div>{data?.title}</div>
      <div>{data?.content}</div>
    </div>
  )
}

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<QueryKey, TPageParam>) => TFnData | Promise<TFnData>`
  - 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<QueryKey, TPageParam>) => TFnData | Promise<TFnData>`
- `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<Data> => {
    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 (
    <div>
      {data.pages.map((group, i) => (
        <React.Fragment key={i}>
          {group.projects.map(project => (
            <p key={project.id}>{project.name}</p>
          ))}
        </React.Fragment>
      ))}
      <div>
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetchingNextPage}
        >
          {isFetchingNextPage
            ? 'Loading more...'
            : hasNextPage
            ? 'Load More'
            : 'Nothing more to load'}
        </button>
      </div>
      <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
    </div>
  )
}

// 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<QueryKey, TPageParam>) => TFnData | Promise<TFnData>`
  - 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<QueryKey, TPageParam>) => TFnData | Promise<TFnData>`
- `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 (
    <div>
      {mutation.isPending ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}

          {mutation.isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutation.mutate({ title: 'Do Laundry', content: 'content...' })
            }}
          >
            create Todo
          </button>
        </>
      )}
    </div>
  )
}

// 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<TData, TVariables>`

## 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<typeof post.list>
type FnData = inferFnData<typeof post.list>
type Variables = inferVariables<typeof post.list>
type Error = inferError<typeof post.list>
```

### 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<QueryHook<Data, Variables>> = 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<Data, Variables>({
  use: [logger],
})

// global middlewares
const queryMiddleware: Middleware<QueryHook> = useQueryNext => {
  return options => {
    // u can also get queryKey via function getKey
    const fullKey = getKey(options.queryKey, options.variables)
    // ...
    return useQueryNext(options)
  }
}
const mutationMiddleware: Middleware<MutationHook> = 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<Data> => {
    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<Data, Variables, Error>({
  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<Data, Variables, Error>(...)

inferData<typeof useProjects> // InfiniteData<Data>
inferFnData<typeof useProjects> // Data
inferVariables<typeof useProjects> // Variables
inferError<typeof useProjects> // Error
inferOptions<typeof useProjects> // 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<string | undefined>()
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

<!-- prettier-ignore-start -->
[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
<!-- prettier-ignore-end -->


================================================
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<UseInfiniteQueryOptions>
): 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<TFnData, TVariables, TError, TPageParam>
): InfiniteQueryHook<TFnData, TVariables, TError, TPageParam> {
  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<TData, TVariables, TError, TContext>
): MutationHook<TData, TVariables, TError, TContext> {
  return Object.assign(
    withMiddleware(ReactQuery.useMutation, defaultOptions, 'mutations'),
    {
      getKey: () => defaultOptions.mutationKey,
      getOptions: () => defaultOptions,
      mutationFn: defaultOptions.mutationFn,
    }
  ) as MutationHook<TData, TVariables, TError, TContext>
}


================================================
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<TFnData, TVariables, TError>
): QueryHook<TFnData, TVariables, TError> {
  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<TFnData, TVariables, TError, TPageParam> {
  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<TFnData, TVariables, TError>
): SuspenseQueryHook<TFnData, TVariables, TError> {
  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 = <TConfig extends RouterConfig>(
  key: string | QueryKey,
  config: TConfig
): CreateRouter<TConfig> => {
  return buildRouter(Array.isArray(key) ? key : [key], config)
}

function query<TFnData, TVariables = void, TError = CompatibleError>(
  options: RouterQueryOptions<TFnData, TVariables, TError>
): RouterQuery<TFnData, TVariables, TError> {
  return {
    ...options,
    _routerType: 'q',
  }
}

function infiniteQuery<
  TFnData,
  TVariables = void,
  TError = CompatibleError,
  TPageParam = number
>(
  options: RouterInfiniteQueryOptions<TFnData, TVariables, TError, TPageParam>
): RouterInfiniteQuery<TFnData, TVariables, TError, TPageParam> {
  return { ...options, _routerType: 'inf' } as RouterInfiniteQuery<
    TFnData,
    TVariables,
    TError,
    TPageParam
  >
}

function mutation<
  TFnData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TContext = unknown
>(
  options: RouterMutationOptions<TFnData, TVariables, TError, TContext>
): RouterMutation<TFnData, TVariables, TError, TContext> {
  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<V5, V4> =
  InfiniteData<unknown> extends UseInfiniteQueryResult<
    InfiniteData<unknown>
  >['data']
    ? V5
    : V4

type CompatibleWithTwoV5<V6Generics, V5Generics> = UseInfiniteQueryOptions<
  unknown,
  DefaultError,
  unknown,
  QueryKey,
  unknown
> extends never
  ? V5Generics
  : V6Generics

type CompatibleUseInfiniteQueryOptions<TFnData, TData, TError, TPageParam> =
  CompatibleWithV4<
    CompatibleWithTwoV5<
      UseInfiniteQueryOptions<TFnData, TError, TData, QueryKey, TPageParam>,
      // @ts-ignore
      UseInfiniteQueryOptions<
        TFnData,
        TError,
        TData,
        TFnData,
        QueryKey,
        TPageParam
      >
    >,
    // @ts-ignore
    UseInfiniteQueryOptions<TFnData, TError, TData, TFnData, QueryKey>
  >

type CompatibleInfiniteData<TFnData, TPageParam> = CompatibleWithV4<
  InfiniteData<TFnData, TPageParam>,
  InfiniteData<TFnData>
>

type NonUndefinedGuard<T> = T extends undefined ? never : T

type WithRequired<T, K extends keyof T> = T & {
  [_ in K]: {}
}

type DeepPartial<T> = T extends object
  ? {
      [P in keyof T]?: DeepPartial<T[P]>
    }
  : T

type DefaultTo<T, D> = unknown extends T ? D : T

export type CompatibleError = CompatibleWithV4<DefaultError, Error>

export type Fetcher<TFnData, TVariables = void, TPageParam = never> = (
  variables: TVariables,
  context: QueryFunctionContext<QueryKey, TPageParam>
) => TFnData | Promise<TFnData>

export type AdditionalQueryOptions<TFnData, TVariables, TPageParam = never> = {
  fetcher: Fetcher<TFnData, TVariables, TPageParam>
  variables?: TVariables
}

type inferMiddlewareHook<T extends (...args: any) => any> = (
  options: inferCreateOptions<T>,
  queryClient?: CompatibleWithV4<QueryClient, void>
) => ReturnType<T>

export type Middleware<
  T extends (...args: any) => any = QueryHook<any, any, any>
> = (hook: inferMiddlewareHook<T>) => inferMiddlewareHook<T>

export type ExposeFetcher<TFnData, TVariables = void, TPageParam = never> = (
  variables: TVariables,
  context?: Partial<QueryFunctionContext<QueryKey, TPageParam>>
) => TFnData | Promise<TFnData>

export type ExposeMethods<TFnData, TVariables, TError, TPageParam = never> = {
  fetcher: ExposeFetcher<TFnData, TVariables, TPageParam>
  getKey: (
    variables?: DeepPartial<TVariables>
  ) => CompatibleWithV4<
    DataTag<
      QueryKey,
      [TPageParam] extends [never] ? TFnData : InfiniteData<TFnData, TPageParam>
    >,
    QueryKey
  >
  getFetchOptions: (
    variables: TVariables extends void
      ? CompatibleWithV4<TVariables | SkipToken, TVariables> | void
      : CompatibleWithV4<TVariables | SkipToken, TVariables>
  ) => Pick<
    ReturnType<
      ExposeMethods<TFnData, TVariables, TError, TPageParam>['getOptions']
    >,
    // @ts-ignore
    [TPageParam] extends [never]
      ? 'queryKey' | 'queryFn' | 'queryKeyHashFn'
      :
          | 'queryKey'
          | 'queryFn'
          | 'queryKeyHashFn'
          | 'getNextPageParam'
          | 'getPreviousPageParam'
          | 'initialPageParam'
  >
  getOptions: (
    variables: TVariables extends void
      ? CompatibleWithV4<TVariables | SkipToken, TVariables> | void
      : CompatibleWithV4<TVariables | SkipToken, TVariables>
  ) => [TPageParam] extends [never]
    ? CompatibleWithV4<
        UseQueryOptions<TFnData, TError, TFnData, QueryKey> & {
          queryKey: DataTag<QueryKey, TFnData>
          queryFn?: Exclude<
            UseQueryOptions<TFnData, TError, TFnData, QueryKey>['queryFn'],
            SkipToken
          >
        },
        // Not work to infer TError in v4
        {
          queryKey: QueryKey
          queryFn: QueryFunction<TFnData, QueryKey>
          queryKeyHashFn?: QueryKeyHashFunction<QueryKey>
        }
      >
    : CompatibleUseInfiniteQueryOptions<
        TFnData,
        TFnData,
        TError,
        TPageParam
      > & {
        queryKey: CompatibleWithV4<
          DataTag<QueryKey, InfiniteData<TFnData, TPageParam>>,
          QueryKey
        >
      }
}

type Clone<T> = T extends infer TClone ? TClone : never

// query hook

export interface CreateQueryOptions<
  TFnData = unknown,
  TVariables = void,
  TError = CompatibleError
> extends Omit<
      UseQueryOptions<TFnData, TError, TFnData, QueryKey>,
      'queryKey' | 'queryFn' | 'select'
    >,
    AdditionalQueryOptions<TFnData, TVariables> {
  queryKey: QueryKey
  use?: Middleware<
    QueryHook<Clone<TFnData>, Clone<TVariables>, Clone<TError>>
  >[]
  variables?: TVariables
}

export interface QueryHookOptions<TFnData, TError, TData, TVariables>
  extends Omit<
    UseQueryOptions<TFnData, TError, TData, QueryKey>,
    'queryKey' | 'queryFn' | 'queryKeyHashFn'
  > {
  use?: Middleware<QueryHook<TFnData, TVariables, TError>>[]
  variables?: CompatibleWithV4<TVariables | SkipToken, TVariables>
}

export interface DefinedQueryHookOptions<TFnData, TError, TData, TVariables>
  extends Omit<
    QueryHookOptions<TFnData, TError, TData, TVariables>,
    'initialData'
  > {
  initialData: NonUndefinedGuard<TFnData> | (() => NonUndefinedGuard<TFnData>)
}

export type QueryHookResult<TData, TError> = UseQueryResult<TData, TError>

export type DefinedQueryHookResult<TData, TError> = DefinedUseQueryResult<
  TData,
  TError
>

export interface QueryHook<
  TFnData = unknown,
  TVariables = void,
  TError = CompatibleError
> extends ExposeMethods<TFnData, TVariables, TError> {
  <TData = TFnData>(
    options: DefinedQueryHookOptions<TFnData, TError, TData, TVariables>,
    queryClient?: CompatibleWithV4<QueryClient, void>
  ): DefinedQueryHookResult<TData, TError>
  <TData = TFnData>(
    options?: QueryHookOptions<TFnData, TError, TData, TVariables>,
    queryClient?: CompatibleWithV4<QueryClient, void>
  ): QueryHookResult<TData, TError>
}

// suspense query hook

export interface CreateSuspenseQueryOptions<
  TFnData = unknown,
  TVariables = void,
  TError = CompatibleError
> extends Omit<
      UseQueryOptions<TFnData, TError, TFnData, QueryKey>,
      | 'queryKey'
      | 'queryFn'
      | 'enabled'
      | 'select'
      | 'suspense'
      | 'throwOnError'
      | 'placeholderData'
      | 'keepPreviousData'
      | 'useErrorBoundary'
    >,
    AdditionalQueryOptions<TFnData, TVariables> {
  queryKey: QueryKey
  use?: Middleware<
    SuspenseQueryHook<Clone<TFnData>, Clone<TVariables>, Clone<TError>>
  >[]
  variables?: TVariables
}

export interface SuspenseQueryHookOptions<TFnData, TError, TData, TVariables>
  extends Omit<
    UseQueryOptions<TFnData, TError, TData, QueryKey>,
    | 'queryKey'
    | 'queryFn'
    | 'queryKeyHashFn'
    | 'enabled'
    | 'suspense'
    | 'throwOnError'
    | 'placeholderData'
    | 'keepPreviousData'
    | 'useErrorBoundary'
  > {
  use?: Middleware<SuspenseQueryHook<TFnData, TVariables, TVariables>>[]
  variables?: CompatibleWithV4<TVariables | SkipToken, TVariables>
}

export type SuspenseQueryHookResult<TData, TError> = Omit<
  QueryObserverSuccessResult<TData, TError>,
  'isPlaceholderData' | 'isPreviousData'
>

export interface SuspenseQueryHook<
  TFnData = unknown,
  TVariables = void,
  TError = CompatibleError
> extends ExposeMethods<TFnData, TVariables, TError> {
  <TData = TFnData>(
    options?: SuspenseQueryHookOptions<TFnData, TError, TData, TVariables>,
    queryClient?: CompatibleWithV4<QueryClient, void>
  ): SuspenseQueryHookResult<TData, TError>
}

// infinite query hook

export interface CreateInfiniteQueryOptions<
  TFnData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TPageParam = number
> extends Omit<
      CompatibleUseInfiniteQueryOptions<TFnData, TFnData, TError, TPageParam>,
      'queryKey' | 'queryFn' | 'select'
    >,
    AdditionalQueryOptions<TFnData, TVariables, TPageParam> {
  queryKey: QueryKey
  use?: Middleware<
    InfiniteQueryHook<
      Clone<TFnData>,
      Clone<TVariables>,
      Clone<TError>,
      Clone<TPageParam>
    >
  >[]
  variables?: TVariables
}

export interface InfiniteQueryHookOptions<
  TFnData,
  TError,
  TData,
  TVariables,
  TPageParam = number
> extends Omit<
    CompatibleUseInfiniteQueryOptions<TFnData, TData, TError, TPageParam>,
    | 'queryKey'
    | 'queryFn'
    | 'queryKeyHashFn'
    | 'initialPageParam'
    | 'getPreviousPageParam'
    | 'getNextPageParam'
  > {
  use?: Middleware<InfiniteQueryHook<TFnData, TVariables, TError, TPageParam>>[]
  variables?: CompatibleWithV4<TVariables | SkipToken, TVariables>
}

export interface DefinedInfiniteQueryHookOptions<
  TFnData,
  TError,
  TData,
  TVariables,
  TPageParam = number
> extends Omit<
    InfiniteQueryHookOptions<TFnData, TError, TData, TVariables, TPageParam>,
    'initialData'
  > {
  initialData:
    | NonUndefinedGuard<CompatibleInfiniteData<TFnData, TPageParam>>
    | (() => NonUndefinedGuard<CompatibleInfiniteData<TFnData, TPageParam>>)
}

export type InfiniteQueryHookResult<TData, TError> = UseInfiniteQueryResult<
  TData,
  TError
>

export type DefinedInfiniteQueryHookResult<TData, TError> = CompatibleWithV4<
  DefinedUseInfiniteQueryResult<TData, TError>,
  WithRequired<UseInfiniteQueryResult<TData, TError>, 'data'>
>

export interface InfiniteQueryHook<
  TFnData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TPageParam = number
> extends ExposeMethods<TFnData, TVariables, TError, TPageParam> {
  <TData = CompatibleWithV4<InfiniteData<TFnData, TPageParam>, TFnData>>(
    options: DefinedInfiniteQueryHookOptions<
      TFnData,
      TError,
      TData,
      TVariables,
      TPageParam
    >,
    queryClient?: CompatibleWithV4<QueryClient, void>
  ): DefinedInfiniteQueryHookResult<TData, TError>
  <TData = CompatibleWithV4<InfiniteData<TFnData, TPageParam>, TFnData>>(
    options?: InfiniteQueryHookOptions<
      TFnData,
      TError,
      TData,
      TVariables,
      TPageParam
    >,
    queryClient?: CompatibleWithV4<QueryClient, void>
  ): InfiniteQueryHookResult<TData, TError>
}

// infinite sususpense query hook

export interface CreateSuspenseInfiniteQueryOptions<
  TFnData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TPageParam = number
> extends Omit<
      CompatibleUseInfiniteQueryOptions<TFnData, TFnData, TError, TPageParam>,
      | 'queryKey'
      | 'queryFn'
      | 'enabled'
      | 'select'
      | 'suspense'
      | 'throwOnError'
      | 'placeholderData'
      | 'keepPreviousData'
      | 'useErrorBoundary'
    >,
    AdditionalQueryOptions<TFnData, TVariables, TPageParam> {
  queryKey: QueryKey
  use?: Middleware<
    SuspenseInfiniteQueryHook<
      Clone<TFnData>,
      Clone<TVariables>,
      Clone<TError>,
      Clone<TPageParam>
    >
  >[]
  variables?: TVariables
}

export interface SuspenseInfiniteQueryHookOptions<
  TFnData,
  TError,
  TData,
  TVariables,
  TPageParam = number
> extends Omit<
    CompatibleUseInfiniteQueryOptions<TFnData, TData, TError, TPageParam>,
    | 'queryKey'
    | 'queryFn'
    | 'queryKeyHashFn'
    | 'enabled'
    | 'initialPageParam'
    | 'getPreviousPageParam'
    | 'getNextPageParam'
    | 'suspense'
    | 'throwOnError'
    | 'placeholderData'
    | 'keepPreviousData'
    | 'useErrorBoundary'
  > {
  use?: Middleware<SuspenseInfiniteQueryHook<TFnData, TVariables, TVariables>>[]
  variables?: CompatibleWithV4<TVariables | SkipToken, TVariables>
}

export type SuspenseInfiniteQueryHookResult<TData, TError> = Omit<
  InfiniteQueryObserverSuccessResult<TData, TError>,
  'isPlaceholderData' | 'isPreviousData'
>

export interface SuspenseInfiniteQueryHook<
  TFnData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TPageParam = number
> extends ExposeMethods<TFnData, TVariables, TError, TPageParam> {
  <TData = CompatibleWithV4<InfiniteData<TFnData, TPageParam>, TFnData>>(
    options?: SuspenseInfiniteQueryHookOptions<
      TFnData,
      TError,
      TData,
      TVariables,
      TPageParam
    >,
    queryClient?: CompatibleWithV4<QueryClient, void>
  ): SuspenseInfiniteQueryHookResult<TData, TError>
}

// mutation hook

export interface CreateMutationOptions<
  TData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TContext = unknown
> extends UseMutationOptions<TData, TError, TVariables, TContext> {
  use?: Middleware<MutationHook<TData, TVariables, TError>>[]
}

export interface MutationHookOptions<TData, TError, TVariables, TContext>
  extends Omit<
    UseMutationOptions<TData, TError, TVariables, TContext>,
    'mutationFn' | 'mutationKey'
  > {
  use?: Middleware<MutationHook<TData, TVariables, TError>>[]
}

export type MutationHookResult<
  TData = unknown,
  TError = CompatibleError,
  TVariables = void,
  TContext = unknown
> = UseMutationResult<TData, TError, TVariables, TContext>

export type ExposeMutationFn<TData = unknown, TVariables = void> = [
  TVariables
] extends [void]
  ? (
      variables?: TVariables,
      context?: Partial<MutationFunctionContext>
    ) => Promise<TData>
  : (
      variables: TVariables,
      context?: Partial<MutationFunctionContext>
    ) => Promise<TData>

export interface ExposeMutationMethods<
  TData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TDefaultContext = unknown
> {
  getKey: () => MutationKey | undefined
  getOptions: () => UseMutationOptions<
    TData,
    TError,
    TVariables,
    TDefaultContext
  >
  mutationFn: ExposeMutationFn<TData, TVariables>
}

export interface MutationHook<
  TData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TDefaultContext = unknown
> extends ExposeMutationMethods<TData, TVariables, TError, TDefaultContext> {
  <TContext = TDefaultContext>(
    options?: MutationHookOptions<TData, TError, TVariables, TContext>,
    queryClient?: CompatibleWithV4<QueryClient, void>
  ): MutationHookResult<TData, TError, TVariables, TContext>
}

// infer types

export type inferVariables<T> = T extends {
  fetcher: ExposeFetcher<any, infer TVariables, infer _TPageParam>
}
  ? TVariables
  : T extends ExposeMutationMethods<any, infer TVariables, any, any>
  ? TVariables
  : never

export type inferData<T> = T extends {
  fetcher: ExposeFetcher<infer TFnData, any, infer TPageParam>
}
  ? [TPageParam] extends [never]
    ? TFnData
    : CompatibleInfiniteData<TFnData, TPageParam>
  : T extends ExposeMutationMethods<infer TFnData, any, any, any>
  ? TFnData
  : never

export type inferFnData<T> = T extends {
  fetcher: ExposeFetcher<infer TFnData, any, infer _TPageParam>
}
  ? TFnData
  : T extends ExposeMutationMethods<infer TFnData, any, any, any>
  ? TFnData
  : never

export type inferError<T> = T extends ExposeMethods<any, any, infer TError>
  ? TError
  : T extends ExposeMethods<any, any, infer TError, any>
  ? TError
  : T extends ExposeMutationMethods<any, any, infer TError, any>
  ? TError
  : never

export type inferOptions<T> = T extends QueryHook<
  infer TFnData,
  infer TVariables,
  infer TError
>
  ? QueryHookOptions<TFnData, TError, TFnData, TVariables>
  : T extends SuspenseQueryHook<infer TFnData, infer TVariables, infer TError>
  ? SuspenseQueryHookOptions<TFnData, TError, TFnData, TVariables>
  : T extends InfiniteQueryHook<
      infer TFnData,
      infer TVariables,
      infer TError,
      infer TPageParam
    >
  ? InfiniteQueryHookOptions<
      TFnData,
      TError,
      CompatibleWithV4<InfiniteData<TFnData, TPageParam>, TFnData>,
      TVariables,
      TPageParam
    >
  : T extends SuspenseInfiniteQueryHook<
      infer TFnData,
      infer TVariables,
      infer TError,
      infer TPageParam
    >
  ? SuspenseInfiniteQueryHookOptions<
      TFnData,
      TError,
      CompatibleWithV4<InfiniteData<TFnData, TPageParam>, TFnData>,
      TVariables,
      TPageParam
    >
  : T extends MutationHook<infer TFnData, infer TVariables, infer TError>
  ? MutationHookOptions<TFnData, TError, TVariables, unknown>
  : never

export type inferCreateOptions<T> = T extends QueryHook<
  infer TFnData,
  infer TVariables,
  infer TError
>
  ? CreateQueryOptions<TFnData, TVariables, TError>
  : T extends SuspenseQueryHook<infer TFnData, infer TVariables, infer TError>
  ? CreateSuspenseQueryOptions<TFnData, TVariables, TError>
  : T extends InfiniteQueryHook<
      infer TFnData,
      infer TVariables,
      infer TError,
      infer TPageParam
    >
  ? CreateInfiniteQueryOptions<TFnData, TVariables, TError, TPageParam>
  : T extends SuspenseInfiniteQueryHook<
      infer TFnData,
      infer TVariables,
      infer TError,
      infer TPageParam
    >
  ? CreateSuspenseInfiniteQueryOptions<TFnData, TVariables, TError, TPageParam>
  : T extends MutationHook<
      infer TFnData,
      infer TVariables,
      infer TError,
      infer TContext
    >
  ? CreateMutationOptions<TFnData, TVariables, TError, TContext>
  : never

// router

export type RouterQueryOptions<
  TFnData,
  TVariables = void,
  TError = CompatibleError
> = Omit<CreateQueryOptions<TFnData, TVariables, TError>, 'queryKey'>

export type RouterQuery<
  TFnData,
  TVariables = void,
  TError = CompatibleError
> = RouterQueryOptions<TFnData, TVariables, TError> & {
  _routerType: `q`
}

export type ResolvedRouterQuery<
  TFnData,
  TVariables = void,
  TError = CompatibleError
> = {
  useQuery: QueryHook<TFnData, TVariables, TError>
  useSuspenseQuery: SuspenseQueryHook<TFnData, TVariables, TError>
} & ExposeMethods<TFnData, TVariables, TError>

export type RouterInfiniteQueryOptions<
  TFnData,
  TVariables = void,
  TError = CompatibleError,
  TPageParam = number
> = Omit<
  CreateInfiniteQueryOptions<TFnData, TVariables, TError, TPageParam>,
  'queryKey'
>

export type RouterInfiniteQuery<
  TFnData,
  TVariables = void,
  TError = CompatibleError,
  TPageParam = number
> = RouterInfiniteQueryOptions<
  TFnData,
  TVariables,
  TError,
  Clone<TPageParam>
> & {
  _routerType: `inf`
}

export type ResolvedRouterInfiniteQuery<
  TFnData,
  TVariables = void,
  TError = CompatibleError,
  TPageParam = number
> = {
  useInfiniteQuery: InfiniteQueryHook<TFnData, TVariables, TError, TPageParam>
  useSuspenseInfiniteQuery: SuspenseInfiniteQueryHook<
    TFnData,
    TVariables,
    TError,
    TPageParam
  >
} & ExposeMethods<TFnData, TVariables, TError, TPageParam>

export type RouterMutationOptions<
  TData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TContext = unknown
> = Omit<
  CreateMutationOptions<TData, TVariables, TError, TContext>,
  'mutationKey'
>

export type RouterMutation<
  TData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TContext = unknown
> = RouterMutationOptions<TData, TVariables, TError, TContext> & {
  _routerType: `m`
}

export type ResolvedRouterMutation<
  TData = unknown,
  TVariables = void,
  TError = CompatibleError,
  TContext = unknown
> = {
  useMutation: MutationHook<
    TData,
    DefaultTo<TVariables, void>,
    DefaultTo<TError, CompatibleError>,
    TContext
  >
} & ExposeMutationMethods<
  TData,
  DefaultTo<TVariables, void>,
  DefaultTo<TError, CompatibleError>,
  TContext
>

export type RouterLeaf =
  | RouterQuery<any, any, any>
  | RouterInfiniteQuery<any, any, any, any>
  | RouterMutation<any, any, any, any>

export type RouterConfig = {
  _routerType?: never
} & {
  [k: string]: RouterLeaf | RouterConfig
}

export type CreateRouter<TConfig extends RouterConfig> = {
  [K in keyof TConfig]: TConfig[K] extends RouterMutation<
    infer TFnData,
    infer TVariables,
    infer TError,
    infer TContext
  >
    ? ResolvedRouterMutation<
        TFnData,
        DefaultTo<TVariables, void>,
        DefaultTo<TError, CompatibleError>,
        TContext
      >
    : TConfig[K] extends RouterInfiniteQuery<
        infer TFnData,
        infer TVariables,
        infer TError,
        infer TPageParam
      >
    ? ResolvedRouterInfiniteQuery<
        TFnData,
        DefaultTo<TVariables, void>,
        DefaultTo<TError, CompatibleError>,
        DefaultTo<TPageParam, number>
      >
    : TConfig[K] extends RouterQuery<
        infer TFnData,
        infer TVariables,
        infer TError
      >
    ? ResolvedRouterQuery<
        TFnData,
        DefaultTo<TVariables, void>,
        DefaultTo<TError, CompatibleError>
      >
    : TConfig[K] extends RouterConfig
    ? CreateRouter<TConfig[K]>
    : never
} & { getKey: () => QueryKey }


================================================
FILE: src/utils.ts
================================================
import * as TanstackReactQuery from '@tanstack/react-query'
import type { Query, QueryClient, QueryKey } from '@tanstack/react-query'

import type { Middleware } from './types'

export const ReactQuery = TanstackReactQuery

export const isV5 = !!ReactQuery.useSuspenseQuery

export const suspenseOptions = {
  enabled: true,
  suspense: true,
  keepPreviousData: undefined,
  useErrorBoundary: (_error: unknown, query: Query) =>
    query.state.data === undefined,
}

export const withMiddleware = (
  hook: any,
  defaultOptions: any,
  type: 'queries' | 'mutations'
) => {
  return function useMiddleware(
    options?: { client?: QueryClient; use?: Middleware[] },
    queryClient?: QueryClient
  ) {
    const [uses, opts]: [Middleware[], any] = [
      ReactQuery.useQueryClient(
        // @ts-ignore Compatible with ReactQuery v4
        isV5 ? queryClient : options
      ).getDefaultOptions()[type],
      defaultOptions,
      options,
    ].reduce(
      ([u1, o1], { use: u2 = [], ...o2 } = {}) => [
        [...u1, ...u2],
        { ...o1, ...o2 },
      ],
      [[]]
    )

    return uses.reduceRight((next, use) => use(next), hook)(opts, queryClient)
  }
}

export const getKey = (queryKey: QueryKey, variables?: any): QueryKey => {
  return variables === undefined ? queryKey : [...queryKey, variables]
}


================================================
FILE: tests/createInfiniteQuery.test.tsx
================================================
import { createInfiniteQuery } from '../src/createInfiniteQuery'
import { omit, uniqueKey } from './utils'

describe('createInfiniteQuery', () => {
  it('should return the correct key', () => {
    type Response = {
      projects: { id: string; name: string }[]
      nextCursor: number
    }
    type Variables = { id: number }

    const key = uniqueKey()
    const variables = { id: 1 }
    const fetcher = (_variables: Variables): Promise<Response> => {
      return fetch(`/test`).then(res => res.json())
    }
    const initialPageParam = 1
    const getNextPageParam = (lastPage: Response) => lastPage.nextCursor
    const useGeneratedQuery = createInfiniteQuery<Response, Variables, Error>({
      queryKey: key,
      fetcher,
      initialPageParam,
      getNextPageParam,
    })

    expect(useGeneratedQuery.getKey()).toEqual(key)
    expect(useGeneratedQuery.getKey(variables)).toEqual([...key, variables])
    expect(omit(useGeneratedQuery.getOptions(variables), 'queryFn')).toEqual({
      queryKey: [...key, variables],
      fetcher,
      initialPageParam,
      getNextPageParam,
    })
    expect(
      omit(useGeneratedQuery.getFetchOptions(variables), 'queryFn')
    ).toEqual({
      queryKey: [...key, variables],
      initialPageParam,
      getNextPageParam,
    })
  })
})


================================================
FILE: tests/createMutation.test.tsx
================================================
import { type MutationKey } from '@tanstack/react-query'

import { createMutation } from '../src/createMutation'

describe('createMutation', () => {
  it('should return the correct key', () => {
    const mutationKey: MutationKey = ['mutationKey']
    const mutation = createMutation({
      mutationKey,
      mutationFn: async () => mutationKey,
    })

    expect(mutation.getKey()).toEqual(mutationKey)
    mutation.mutationFn().then(data => expect(data).toEqual(mutationKey))
  })
})


================================================
FILE: tests/createQuery.test.tsx
================================================
import { QueryClient, skipToken } from '@tanstack/react-query'
import '@testing-library/jest-dom'
import { fireEvent, waitFor } from '@testing-library/react'
import * as React from 'react'

import { createQuery } from '../src'
import type { QueryHookResult } from '../src'
import { Middleware } from '../src/types'
import { omit, renderWithClient, sleep, uniqueKey } from './utils'

describe('createQuery', () => {
  const queryClient = new QueryClient()

  it('should return the correct key', () => {
    const key = uniqueKey()
    const variables = { id: 1 }
    const fetcher = (_variables: { id: number }) => {
      return 'test'
    }
    const useGeneratedQuery = createQuery({
      queryKey: key,
      fetcher,
    })

    expect(useGeneratedQuery.getKey()).toEqual(key)
    expect(useGeneratedQuery.getKey(variables)).toEqual([...key, variables])
    expect(omit(useGeneratedQuery.getOptions(variables), 'queryFn')).toEqual({
      queryKey: [...key, variables],
      fetcher,
    })
    expect(
      omit(useGeneratedQuery.getFetchOptions(variables), 'queryFn')
    ).toEqual({
      queryKey: [...key, variables],
    })

    queryClient.prefetchQuery(useGeneratedQuery.getFetchOptions(variables))
  })

  it('should return the correct initial data from middleware', async () => {
    const myMiddileware: Middleware = useQueryNext => {
      return options => {
        return useQueryNext({
          ...options,
          initialData: 'initialData',
          enabled: false,
        })
      }
    }

    const useGeneratedQuery = createQuery({
      queryKey: uniqueKey(),
      fetcher: (_variables: { id: number }) => {
        return 'test'
      },
      use: [
        useNext => {
          return options =>
            useNext({
              ...options,
              initialData: 'fakeData',
              enabled: false,
            })
        },
        myMiddileware,
      ],
    })

    const states: QueryHookResult<any, any>[] = []

    function Page() {
      const state = useGeneratedQuery()

      states.push(state)

      return <span>{state.data}</span>
    }

    const rendered = renderWithClient(queryClient, <Page />)

    await waitFor(() => rendered.getByText('initialData'))
  })

  it('should return the correct initial data', async () => {
    const useGeneratedQuery = createQuery<string, { id: number }>({
      queryKey: uniqueKey(),
      fetcher: () => {
        return 'test'
      },
      use: [
        useNext => {
          return options =>
            useNext({
              ...options,
              initialData: options.initialData ?? 'initialData',
              enabled: false,
            })
        },
      ],
    })
    const states: QueryHookResult<any, any>[] = []

    function Page() {
      const state = useGeneratedQuery({ initialData: 'stateData' })

      states.push(state)

      return <span>{state.data}</span>
    }

    const rendered = renderWithClient(queryClient, <Page />)

    await waitFor(() => rendered.getByText('stateData'))
  })

  it('should return the selected data', async () => {
    const useGeneratedQuery = createQuery<string>({
      queryKey: uniqueKey(),
      fetcher: () => {
        return 'test'
      },
    })
    const states: QueryHookResult<any, any>[] = []

    function Page() {
      const state = useGeneratedQuery({
        select() {
          return 'selectedData'
        },
      })

      states.push(state)

      return <span>{state.data}</span>
    }

    const rendered = renderWithClient(queryClient, <Page />)

    await waitFor(() => rendered.getByText('selectedData'))
  })

  it('should respect skipToken and refetch when skipToken is taken away', async () => {
    const useGeneratedQuery = createQuery<string>({
      queryKey: uniqueKey(),
      fetcher: async () => {
        await sleep(10)
        return Promise.resolve('data')
      },
    })

    function Page({ enabled }: { enabled: boolean }) {
      const { data, status } = useGeneratedQuery({
        variables: enabled ? undefined : skipToken,
        retry: false,
        retryOnMount: false,
        refetchOnMount: false,
        refetchOnWindowFocus: false,
      })

      return (
        <div>
          <div>status: {status}</div>
          <div>data: {String(data)}</div>
        </div>
      )
    }

    function App() {
      const [enabled, toggle] = React.useReducer(x => !x, false)

      return (
        <div>
          <Page enabled={enabled} />
          <button onClick={toggle}>enable</button>
        </div>
      )
    }

    const rendered = renderWithClient(queryClient, <App />)

    await waitFor(() => rendered.getByText('status: pending'))

    fireEvent.click(rendered.getByRole('button', { name: 'enable' }))
    await waitFor(() => rendered.getByText('status: success'))
    await waitFor(() => rendered.getByText('data: data'))
  })
})


================================================
FILE: tests/router.test.tsx
================================================
import { router } from '../src'

describe('router', () => {
  it('should return the correct shape', () => {
    const post = router(`post`, {
      byId: router.query({
        fetcher: (variables: {
          id: number
        }): Promise<{ title: string; content: string }> =>
          fetch(`/post/${variables.id}`).then(res => res.json()),
      }),

      list: router.infiniteQuery({
        fetcher: (_variables, { pageParam }) =>
          fetch(`/post/?cursor=${pageParam}`).then(res => res.json()),
        getNextPageParam: lastPage => lastPage.nextCursor,
        initialPageParam: 0,
      }),

      add: router.mutation({
        mutationFn: async (variables: {
          title: string
          content: string
        }): Promise<{
          ret: number
        }> =>
          fetch('/post', {
            method: 'POST',
            body: JSON.stringify(variables),
          }).then(res => res.json()),
      }),

      command: {
        report: router.query({
          fetcher: (variables: {
            id: number
          }): Promise<{ title: string; content: string }> =>
            fetch(`/post/report/${variables.id}`).then(res => res.json()),
        }),
      },
    })

    expect(post.getKey()).toEqual(['post'])
    expect(post.byId.getKey()).toEqual(['post', 'byId'])
    expect(post.byId.getKey({ id: 1 })).toEqual(['post', 'byId', { id: 1 }])
    expect(post.list.getKey()).toEqual(['post', 'list'])
    expect(post.add.getKey()).toEqual(['post', 'add'])
    expect(post.command.getKey()).toEqual(['post', 'command'])
    expect(post.command.report.getKey()).toEqual(['post', 'command', 'report'])
    expect(post.command.report.getKey({ id: 1 })).toEqual([
      'post',
      'command',
      'report',
      { id: 1 },
    ])
    expect(typeof post.command.report.fetcher === 'function').toBe(true)
    expect(typeof post.command.report.getFetchOptions === 'function').toBe(true)
    expect(typeof post.command.report.getOptions === 'function').toBe(true)
    expect(typeof post.command.report.useQuery === 'function').toBe(true)
    expect(typeof post.command.report.useSuspenseQuery === 'function').toBe(
      true
    )
    expect(typeof post.byId.fetcher === 'function').toBe(true)
    expect(typeof post.byId.getFetchOptions === 'function').toBe(true)
    expect(typeof post.byId.getOptions === 'function').toBe(true)
    expect(typeof post.byId.useQuery === 'function').toBe(true)
    expect(typeof post.byId.useSuspenseQuery === 'function').toBe(true)
    expect(typeof post.list.fetcher === 'function').toBe(true)
    expect(typeof post.list.getFetchOptions === 'function').toBe(true)
    expect(typeof post.list.getOptions === 'function').toBe(true)
    expect(typeof post.list.useInfiniteQuery === 'function').toBe(true)
    expect(typeof post.list.useSuspenseInfiniteQuery === 'function').toBe(true)
    expect(typeof post.add.mutationFn === 'function').toBe(true)
    expect(typeof post.add.getKey === 'function').toBe(true)
    expect(typeof post.add.getOptions === 'function').toBe(true)
    expect(typeof post.add.useMutation === 'function').toBe(true)
  })

  it('should return the correct shape when pass a array keys', () => {
    const post = router(['scope', `post`], {
      byId: router.query({
        fetcher: (variables: {
          id: number
        }): Promise<{ title: string; content: string }> =>
          fetch(`/post/${variables.id}`).then(res => res.json()),
      }),

      list: router.infiniteQuery({
        fetcher: (_variables, { pageParam }) =>
          fetch(`/post/?cursor=${pageParam}`).then(res => res.json()),
        getNextPageParam: lastPage => lastPage.nextCursor,
        initialPageParam: 0,
      }),

      add: router.mutation({
        mutationFn: async (variables: {
          title: string
          content: string
        }): Promise<{
          ret: number
        }> =>
          fetch('/post', {
            method: 'POST',
            body: JSON.stringify(variables),
          }).then(res => res.json()),
      }),

      command: {
        report: router.query({
          fetcher: (variables: {
            id: number
          }): Promise<{ title: string; content: string }> =>
            fetch(`/post/report/${variables.id}`).then(res => res.json()),
        }),
      },
    })

    expect(post.getKey()).toEqual(['scope', 'post'])
    expect(post.byId.getKey()).toEqual(['scope', 'post', 'byId'])
    expect(post.byId.getKey({ id: 1 })).toEqual([
      'scope',
      'post',
      'byId',
      { id: 1 },
    ])
    expect(post.list.getKey()).toEqual(['scope', 'post', 'list'])
    expect(post.add.getKey()).toEqual(['scope', 'post', 'add'])
    expect(post.command.getKey()).toEqual(['scope', 'post', 'command'])
    expect(post.command.report.getKey()).toEqual([
      'scope',
      'post',
      'command',
      'report',
    ])
    expect(post.command.report.getKey({ id: 1 })).toEqual([
      'scope',
      'post',
      'command',
      'report',
      { id: 1 },
    ])
    expect(typeof post.command.report.fetcher === 'function').toBe(true)
    expect(typeof post.command.report.getFetchOptions === 'function').toBe(true)
    expect(typeof post.command.report.getOptions === 'function').toBe(true)
    expect(typeof post.command.report.useQuery === 'function').toBe(true)
    expect(typeof post.command.report.useSuspenseQuery === 'function').toBe(
      true
    )
    expect(typeof post.byId.fetcher === 'function').toBe(true)
    expect(typeof post.byId.getFetchOptions === 'function').toBe(true)
    expect(typeof post.byId.getOptions === 'function').toBe(true)
    expect(typeof post.byId.useQuery === 'function').toBe(true)
    expect(typeof post.byId.useSuspenseQuery === 'function').toBe(true)
    expect(typeof post.list.fetcher === 'function').toBe(true)
    expect(typeof post.list.getFetchOptions === 'function').toBe(true)
    expect(typeof post.list.getOptions === 'function').toBe(true)
    expect(typeof post.list.useInfiniteQuery === 'function').toBe(true)
    expect(typeof post.list.useSuspenseInfiniteQuery === 'function').toBe(true)
    expect(typeof post.add.mutationFn === 'function').toBe(true)
    expect(typeof post.add.getKey === 'function').toBe(true)
    expect(typeof post.add.getOptions === 'function').toBe(true)
    expect(typeof post.add.useMutation === 'function').toBe(true)
  })
})


================================================
FILE: tests/types.typecheck.ts
================================================
import { createQuery, router } from '../src'
import type { Middleware } from '../src'

const usePost = createQuery<{ title: string }, { id: number }>({
  queryKey: ['post'],
  fetcher: async variables => ({
    title: `post-${variables.id}`,
  }),
})

const queryMiddleware: Middleware<typeof usePost> = useQueryNext => options => {
  options.fetcher
  options.variables?.id

  // @ts-expect-error Query middleware should not expose mutation options.
  options.mutationFn

  return useQueryNext(options)
}

void queryMiddleware

type UsePostOptions = Parameters<typeof usePost>[0]

const validQueryOptions: UsePostOptions = {
  variables: { id: 1 },
}

void validQueryOptions

const invalidQueryOptions: UsePostOptions = {
  // @ts-expect-error Query hooks should not accept mutation options.
  mutationFn: async () => ({ title: 'post-1' }),
}

void invalidQueryOptions

const postRoutes = router('post', {
  byId: router.query({
    fetcher: async (variables: { id: number }) => ({
      id: variables.id,
      title: `post-${variables.id}`,
    }),
  }),
  add: router.mutation({
    mutationFn: async (variables: { title: string }) => ({
      title: variables.title,
    }),
  }),
})

postRoutes.byId.useQuery({
  variables: { id: 1 },
  enabled: false,
})

postRoutes.add.useMutation({
  onSuccess(data) {
    const title: string = data.title

    void title
  },
})

// @ts-expect-error Query routes should not expose mutation hooks.
postRoutes.byId.useMutation()

// @ts-expect-error Mutation routes should not expose query hooks.
postRoutes.add.useQuery()


================================================
FILE: tests/utils.tsx
================================================
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render } from '@testing-library/react'
import * as React from 'react'

let queryKeyCount = 0
export function uniqueKey(): string[] {
  queryKeyCount++
  return [`query_${queryKeyCount}`]
}

export function renderWithClient(
  client: QueryClient,
  ui: React.ReactElement
): ReturnType<typeof render> {
  const { rerender, ...result } = render(
    <QueryClientProvider client={client}>{ui}</QueryClientProvider>
  )
  return {
    ...result,
    rerender: (rerenderUi: React.ReactElement) =>
      rerender(
        <QueryClientProvider client={client}>{rerenderUi}</QueryClientProvider>
      ),
  } as any
}

export function omit<T extends object, K extends string[]>(
  object: T | null | undefined,
  ...paths: K
): Pick<T, Exclude<keyof T, K[number]>> {
  return Object.fromEntries(
    Object.entries(object || {}).filter(([key]) => !paths.includes(key))
  ) as Pick<T, Exclude<keyof T, K[number]>>
}

export function sleep(timeout: number): Promise<void> {
  return new Promise((resolve, _reject) => {
    setTimeout(resolve, timeout)
  })
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "noImplicitAny": true,
    "noImplicitReturns": false,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noUncheckedIndexedAccess": true,
    "strictNullChecks": true,
    "jsx": "react",
    "declaration": true,
    "emitDeclarationOnly": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["jest", "node"],
    "outDir": "./build/lib"
  },
  "files": ["src/index.ts"],
  "include": ["src"]
}


================================================
FILE: tsconfig.types.json
================================================
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "declaration": false,
    "emitDeclarationOnly": false,
    "noEmit": true,
    "outDir": "./build/types"
  },
  "files": ["tests/types.typecheck.ts"]
}
Download .txt
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
Download .txt
SYMBOL INDEX (62 symbols across 13 files)

FILE: rollup.config.js
  function rollup (line 22) | function rollup() {
  function mjs (line 41) | function mjs({ input, external }) {
  function esm (line 57) | function esm({ input, external }) {
  function cjs (line 73) | function cjs({ input, external }) {
  function umdDev (line 90) | function umdDev({ input, external, globals, jsName }) {
  function umdProd (line 111) | function umdProd({ input, external, globals, jsName }) {

FILE: src/createBaseQuery.ts
  type QueryBaseHookOptions (line 10) | type QueryBaseHookOptions = Omit<

FILE: src/createInfiniteQuery.ts
  function createInfiniteQuery (line 9) | function createInfiniteQuery<

FILE: src/createMutation.ts
  function createMutation (line 8) | function createMutation<

FILE: src/createQuery.ts
  function createQuery (line 5) | function createQuery<

FILE: src/createSuspenseInfiniteQuery.ts
  function createSuspenseInfiniteQuery (line 9) | function createSuspenseInfiniteQuery<

FILE: src/createSuspenseQuery.ts
  function createSuspenseQuery (line 9) | function createSuspenseQuery<

FILE: src/router.ts
  type RouterNode (line 20) | type RouterNode = RouterConfig[string]
  type RouterTypedNode (line 21) | type RouterTypedNode = RouterNode & {
  function query (line 78) | function query<TFnData, TVariables = void, TError = CompatibleError>(
  function infiniteQuery (line 87) | function infiniteQuery<
  function mutation (line 103) | function mutation<

FILE: src/types.ts
  type CompatibleWithV4 (line 27) | type CompatibleWithV4<V5, V4> =
  type CompatibleWithTwoV5 (line 34) | type CompatibleWithTwoV5<V6Generics, V5Generics> = UseInfiniteQueryOptions<
  type CompatibleUseInfiniteQueryOptions (line 44) | type CompatibleUseInfiniteQueryOptions<TFnData, TData, TError, TPagePara...
  type CompatibleInfiniteData (line 62) | type CompatibleInfiniteData<TFnData, TPageParam> = CompatibleWithV4<
  type NonUndefinedGuard (line 67) | type NonUndefinedGuard<T> = T extends undefined ? never : T
  type WithRequired (line 69) | type WithRequired<T, K extends keyof T> = T & {
  type DeepPartial (line 73) | type DeepPartial<T> = T extends object
  type DefaultTo (line 79) | type DefaultTo<T, D> = unknown extends T ? D : T
  type CompatibleError (line 81) | type CompatibleError = CompatibleWithV4<DefaultError, Error>
  type Fetcher (line 83) | type Fetcher<TFnData, TVariables = void, TPageParam = never> = (
  type AdditionalQueryOptions (line 88) | type AdditionalQueryOptions<TFnData, TVariables, TPageParam = never> = {
  type inferMiddlewareHook (line 93) | type inferMiddlewareHook<T extends (...args: any) => any> = (
  type Middleware (line 98) | type Middleware<
  type ExposeFetcher (line 102) | type ExposeFetcher<TFnData, TVariables = void, TPageParam = never> = (
  type ExposeMethods (line 107) | type ExposeMethods<TFnData, TVariables, TError, TPageParam = never> = {
  type Clone (line 170) | type Clone<T> = T extends infer TClone ? TClone : never
  type CreateQueryOptions (line 174) | interface CreateQueryOptions<
  type QueryHookOptions (line 190) | interface QueryHookOptions<TFnData, TError, TData, TVariables>
  type DefinedQueryHookOptions (line 199) | interface DefinedQueryHookOptions<TFnData, TError, TData, TVariables>
  type QueryHookResult (line 207) | type QueryHookResult<TData, TError> = UseQueryResult<TData, TError>
  type DefinedQueryHookResult (line 209) | type DefinedQueryHookResult<TData, TError> = DefinedUseQueryResult<
  type QueryHook (line 214) | interface QueryHook<
  type CreateSuspenseQueryOptions (line 231) | interface CreateSuspenseQueryOptions<
  type SuspenseQueryHookOptions (line 255) | interface SuspenseQueryHookOptions<TFnData, TError, TData, TVariables>
  type SuspenseQueryHookResult (line 272) | type SuspenseQueryHookResult<TData, TError> = Omit<
  type SuspenseQueryHook (line 277) | interface SuspenseQueryHook<
  type CreateInfiniteQueryOptions (line 290) | interface CreateInfiniteQueryOptions<
  type InfiniteQueryHookOptions (line 312) | interface InfiniteQueryHookOptions<
  type DefinedInfiniteQueryHookOptions (line 331) | interface DefinedInfiniteQueryHookOptions<
  type InfiniteQueryHookResult (line 346) | type InfiniteQueryHookResult<TData, TError> = UseInfiniteQueryResult<
  type DefinedInfiniteQueryHookResult (line 351) | type DefinedInfiniteQueryHookResult<TData, TError> = CompatibleWithV4<
  type InfiniteQueryHook (line 356) | interface InfiniteQueryHook<

FILE: tests/createInfiniteQuery.test.tsx
  type Response (line 6) | type Response = {
  type Variables (line 10) | type Variables = { id: number }

FILE: tests/createQuery.test.tsx
  function Page (line 71) | function Page() {
  function Page (line 103) | function Page() {
  function Page (line 125) | function Page() {
  function Page (line 151) | function Page({ enabled }: { enabled: boolean }) {
  function App (line 168) | function App() {

FILE: tests/types.typecheck.ts
  type UsePostOptions (line 23) | type UsePostOptions = Parameters<typeof usePost>[0]
  method onSuccess (line 58) | onSuccess(data) {

FILE: tests/utils.tsx
  function uniqueKey (line 6) | function uniqueKey(): string[] {
  function renderWithClient (line 11) | function renderWithClient(
  function omit (line 27) | function omit<T extends object, K extends string[]>(
  function sleep (line 36) | function sleep(timeout: number): Promise<void> {
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (105K chars).
[
  {
    "path": ".browserslistrc",
    "chars": 99,
    "preview": "# Browsers we support\nChrome >= 73\nFirefox >= 78\nEdge >= 79\nSafari >= 12.0\niOS >= 12.0\nopera >= 53\n"
  },
  {
    "path": ".commitlintrc.json",
    "chars": 53,
    "preview": "{\n  \"extends\": [\"@commitlint/config-conventional\"]\n}\n"
  },
  {
    "path": ".eslintrc",
    "chars": 2456,
    "preview": "{\n  \"env\": {\n    \"browser\": true,\n    \"shared-node-browser\": true,\n    \"node\": true,\n    \"es6\": true\n  },\n  \"extends\": ["
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 606,
    "preview": "name: 'publish'\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  release:\n    name: publish\n    runs-on: ubuntu-latest\n "
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 798,
    "preview": "name: Tests\n\non:\n  push:\n    branches:\n      - main\njobs:\n  tests:\n    name: Building package\n    runs-on: ubuntu-latest"
  },
  {
    "path": ".gitignore",
    "chars": 167,
    "preview": "node_modules\n\n# builds\nbuild\n\n# misc\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nyarn.lock\npackage-lock.json\nsize-plu"
  },
  {
    "path": ".husky/commit-msg",
    "chars": 76,
    "preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx commitlint --edit \n"
  },
  {
    "path": ".npmrc",
    "chars": 59,
    "preview": "auto-install-peers=true\nregistry=https://registry.npmjs.org"
  },
  {
    "path": ".nvmrc",
    "chars": 8,
    "preview": "v16.19.0"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2022 liaoliao666\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README-zh_CN.md",
    "chars": 17452,
    "preview": "<div align=\"center\">\n\n<br />\n<br />\n\n<p align=\"center\">\n  <a aria-label=\"NPM version\" href=\"./assets/logo.svg\">\n    <img"
  },
  {
    "path": "README.md",
    "chars": 19994,
    "preview": "<div align=\"center\">\n\n<br />\n<br />\n\n<p align=\"center\">\n  <a aria-label=\"NPM version\" href=\"./assets/logo.svg\">\n    <img"
  },
  {
    "path": "babel.config.js",
    "chars": 300,
    "preview": "module.exports = {\n  presets: [\n    [\n      '@babel/preset-env',\n      {\n        loose: true,\n        modules: false,\n  "
  },
  {
    "path": "jest.config.js",
    "chars": 159,
    "preview": "/** @type {import('jest').Config} */\nconst config = {\n  transform: {\n    '\\\\.[jt]sx?$': 'ts-jest',\n  },\n  testEnvironmen"
  },
  {
    "path": "package.json",
    "chars": 2578,
    "preview": "{\n  \"name\": \"react-query-kit\",\n  \"version\": \"3.3.3\",\n  \"description\": \"🕊️ A toolkit for ReactQuery that make ReactQuery "
  },
  {
    "path": "prettier.config.js",
    "chars": 385,
    "preview": "module.exports = {\n  printWidth: 80,\n  tabWidth: 2,\n  useTabs: false,\n  semi: false,\n  singleQuote: true,\n  trailingComm"
  },
  {
    "path": "rollup.config.js",
    "chars": 2998,
    "preview": "import { babel } from '@rollup/plugin-babel'\nimport commonJS from '@rollup/plugin-commonjs'\nimport { nodeResolve } from "
  },
  {
    "path": "src/createBaseQuery.ts",
    "chars": 2311,
    "preview": "import {\n  type QueryClient,\n  type QueryFunctionContext,\n  type UseBaseQueryOptions,\n  type UseInfiniteQueryOptions,\n} "
  },
  {
    "path": "src/createInfiniteQuery.ts",
    "chars": 521,
    "preview": "import { createBaseQuery } from './createBaseQuery'\nimport type {\n  CompatibleError,\n  CreateInfiniteQueryOptions,\n  Inf"
  },
  {
    "path": "src/createMutation.ts",
    "chars": 702,
    "preview": "import type {\n  CompatibleError,\n  CreateMutationOptions,\n  MutationHook,\n} from './types'\nimport { ReactQuery, withMidd"
  },
  {
    "path": "src/createQuery.ts",
    "chars": 419,
    "preview": "import { createBaseQuery } from './createBaseQuery'\nimport type { CompatibleError, CreateQueryOptions, QueryHook } from "
  },
  {
    "path": "src/createSuspenseInfiniteQuery.ts",
    "chars": 700,
    "preview": "import { createBaseQuery } from './createBaseQuery'\nimport type {\n  CompatibleError,\n  CreateSuspenseInfiniteQueryOption"
  },
  {
    "path": "src/createSuspenseQuery.ts",
    "chars": 577,
    "preview": "import { createBaseQuery } from './createBaseQuery'\nimport type {\n  CompatibleError,\n  CreateSuspenseQueryOptions,\n  Sus"
  },
  {
    "path": "src/index.ts",
    "chars": 267,
    "preview": "export * from './createQuery'\nexport * from './createSuspenseQuery'\nexport * from './createInfiniteQuery'\nexport * from "
  },
  {
    "path": "src/router.ts",
    "chars": 3188,
    "preview": "import { QueryKey } from '@tanstack/react-query'\n\nimport { createInfiniteQuery } from './createInfiniteQuery'\nimport { c"
  },
  {
    "path": "src/types.ts",
    "chars": 20768,
    "preview": "import type {\n  DataTag,\n  DefaultError,\n  DefinedUseInfiniteQueryResult,\n  DefinedUseQueryResult,\n  InfiniteData,\n  Inf"
  },
  {
    "path": "src/utils.ts",
    "chars": 1323,
    "preview": "import * as TanstackReactQuery from '@tanstack/react-query'\nimport type { Query, QueryClient, QueryKey } from '@tanstack"
  },
  {
    "path": "tests/createInfiniteQuery.test.tsx",
    "chars": 1304,
    "preview": "import { createInfiniteQuery } from '../src/createInfiniteQuery'\nimport { omit, uniqueKey } from './utils'\n\ndescribe('cr"
  },
  {
    "path": "tests/createMutation.test.tsx",
    "chars": 489,
    "preview": "import { type MutationKey } from '@tanstack/react-query'\n\nimport { createMutation } from '../src/createMutation'\n\ndescri"
  },
  {
    "path": "tests/createQuery.test.tsx",
    "chars": 4876,
    "preview": "import { QueryClient, skipToken } from '@tanstack/react-query'\nimport '@testing-library/jest-dom'\nimport { fireEvent, wa"
  },
  {
    "path": "tests/router.test.tsx",
    "chars": 6352,
    "preview": "import { router } from '../src'\n\ndescribe('router', () => {\n  it('should return the correct shape', () => {\n    const po"
  },
  {
    "path": "tests/types.typecheck.ts",
    "chars": 1565,
    "preview": "import { createQuery, router } from '../src'\nimport type { Middleware } from '../src'\n\nconst usePost = createQuery<{ tit"
  },
  {
    "path": "tests/utils.tsx",
    "chars": 1132,
    "preview": "import { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { render } from '@testing-library/react'"
  },
  {
    "path": "tsconfig.json",
    "chars": 677,
    "preview": "{\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2020\"],\n    \"target\": \"ES2020\",\n    \"module\": \"ES2020\",\n "
  },
  {
    "path": "tsconfig.types.json",
    "chars": 214,
    "preview": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": false,\n    \"emitDeclarationOnly\": false,\n   "
  }
]

About this extraction

This page contains the full source code of the liaoliao666/react-query-kit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (94.4 KB), approximately 26.8k tokens, and a symbol index with 62 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!