Full Code of jotaijs/jotai-tanstack-query for AI

main 06759979819e cached
111 files
153.1 KB
45.4k tokens
57 symbols
1 requests
Download .txt
Repository: jotaijs/jotai-tanstack-query
Branch: main
Commit: 06759979819e
Files: 111
Total size: 153.1 KB

Directory structure:
gitextract_m_gm3mha/

├── .github/
│   └── workflows/
│       ├── cd.yml
│       ├── ci.yml
│       └── pr.yml
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__/
│   ├── 01_basic.spec.tsx
│   ├── atomWithInfiniteQuery.spec.tsx
│   ├── atomWithMutation.spec.tsx
│   ├── atomWithMutationState.spec.tsx
│   ├── atomWithQuery.spec.tsx
│   ├── atomWithSuspenseInfiniteQuery.spec.tsx
│   └── atomWithSuspenseQuery.spec.tsx
├── eslint.config.js
├── examples/
│   ├── 01_query/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 02_suspense/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 03_infinite/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 04_infinite_suspense/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 05_mutation/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 06_refetch/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 07_queries/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 08_query_client_atom_provider/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 09_error_boundary/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   └── 11_nextjs_app_router/
│       ├── .gitignore
│       ├── README.md
│       ├── eslint.config.mjs
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── src/
│       │   └── app/
│       │       ├── api.ts
│       │       ├── layout.tsx
│       │       ├── page.tsx
│       │       ├── posts/
│       │       │   ├── [postId]/
│       │       │   │   ├── _components/
│       │       │   │   │   └── post.tsx
│       │       │   │   └── page.tsx
│       │       │   └── page.tsx
│       │       ├── providers.tsx
│       │       └── stores.ts
│       └── tsconfig.json
├── package.json
├── pnpm-workspace.yaml
├── src/
│   ├── _queryClientAtom.ts
│   ├── atomWithInfiniteQuery.ts
│   ├── atomWithMutation.ts
│   ├── atomWithMutationState.ts
│   ├── atomWithQueries.ts
│   ├── atomWithQuery.ts
│   ├── atomWithSuspenseInfiniteQuery.ts
│   ├── atomWithSuspenseQuery.ts
│   ├── baseAtomWithQuery.ts
│   ├── index.ts
│   ├── react.ts
│   ├── types.ts
│   └── utils.ts
├── tsconfig.json
└── vitest.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/cd.yml
================================================
name: CD

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: 'https://registry.npmjs.org'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - run: pnpm install --frozen-lockfile
      - run: pnpm run compile
      - run: pnpm test
      - run: pnpm publish --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - run: pnpm install --frozen-lockfile
      - run: pnpm compile
      - run: pnpm test


================================================
FILE: .github/workflows/pr.yml
================================================
name: Publish Preview

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - run: pnpm install --frozen-lockfile
      - run: pnpm run compile
      - run: pnpm dlx pkg-pr-new publish --template './examples/*'


================================================
FILE: .gitignore
================================================
*~
*.swp
node_modules
/dist


================================================
FILE: .prettierrc
================================================
{
  "semi": false,
  "trailingComma": "es5",
  "singleQuote": true,
  "bracketSameLine": true,
  "tabWidth": 2,
  "printWidth": 80
}


================================================
FILE: CHANGELOG.md
================================================
# Change Log

## [Unreleased]

## [0.11.0] - 2025-08-01

### Changed

- feat: `QueryClientAtomProvider` is a ready-to-use wrapper that combines Jotai Provider and TanStack Query QueryClientProvider.
- breaking: removed `QueryClientProvider`

## [0.10.1] - 2025-07-24

### Changed

- fix: '/react' import path for QueryClientProvider utils

## [0.10.0] - 2025-07-23

### Changed

- feat: atomWithQueries for tanstack/query's useQueries
- feat: add QueryClientProvider utils for react

## [0.9.2] - 2025-07-10

### Changed

- upgrade @tankstack/query-core to 5.81.5

## [0.9.1] - 2025-07-09

### Changed

- upgrade @tankstack/query-core to 5.81.2

## [0.9.0] - 2024-09-29

### Added

- add: change atom signature to return a setter function to force re-evaluate an atom. This is to help the atom able to reevaluate when it hits an error boundary by throwing a rejected promise.

## [0.8.8] - 2024-09-15

### Changed

- fix: use query.reset on error instead of client.resetQueries.

## [0.8.7] - 2024-08-18

### Changed

- add: batch calls to improve perf
- fix: return appropriate cleanup functions
- fix: cleanup - remove unused code

## [0.8.6] - 2024-08-05

### Changed

- fix: remove wonka as a peer dependency

## [0.8.5] - 2024-02-04

### Changed

- fix: include types.ts in build

## [0.8.4] - 2024-01-31

### Changed

- fix: fix generics

## [0.8.3] - 2024-01-29

### Changed

- fix: update types

## [0.8.2] - 2024-01-15

### Changed

- fix: update jotai peer dependency to v2

## [0.8.1] - 2023-12-23

### Changed

- fix: add default staletime for suspense atoms
- fix: suspense example

## [0.8.0] - 2023-12-09

### Added

- breaking: update atom api to resemble tanstack/query api
- add: atomWithSuspenseQuery, atomWithSuspenseInfiniteQuery, atomWithMutationState

## [0.7.2] - 2023-09-08

### Changed

- fix: loading mutation does not call refresh on unmount #38

## [0.7.1] - 2023-05-25

### Changed

- Fix result of statusAtom sometimes not updated #35

## [0.7.0] - 2023-04-03

### Added

- feat: atomsWithQueryAsync, plus example #30

## [0.6.0] - 2023-03-03

### Added

- feat: mark internal atoms as private

## [0.5.0] - 2023-01-31

### Added

- Migrate to Jotai v2 API #18

## [0.4.0] - 2022-10-21

### Changed

- fix: status should change #10
- breaking: simplify api names #11

## [0.3.0] - 2022-10-11

### Changed

- make mutation atom type correct #7
- update jotai and fix types #8

## [0.2.1] - 2022-10-04

### Changed

- fix setOptions not to delay #6

## [0.2.0] - 2022-09-27

### Changed

- for dataAtom, re-create observer when options change #5

## [0.1.0] - 2022-09-24

### Added

- implement refetch #1
- feat: observer cache #2
- feat: infinite query #3
- feat: mutation api #4

## [0.0.1] - 2022-09-20

### Added

- Initial experimental release


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2022 Daishi Kato

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.md
================================================
# Jotai Query 🚀 👻

[jotai-tanstack-query](https://github.com/jotai-labs/jotai-tanstack-query) is a Jotai extension library for TanStack Query. It provides a wonderful interface with all of the TanStack Query features, providing you the ability to use those features in combination with your existing Jotai state.

# Table of contents

- [Support](#support)
- [Install](#install)
- [Usage](#usage)
- [Incremental Adoption](#incremental-adoption)
- [Exported Provider](#exported-provider)
- [Exported Functions](#exported-functions)
  - [atomWithQuery](#atomwithquery-usage)
  - [atomWithQueries](#atomwithqueries-usage)
  - [atomWithInfiniteQuery](#atomwithinfinitequery-usage)
  - [atomWithMutation](#atomwithmutation-usage)
  - [atomWithMutationState](#atomwithmutationstate-usage)
  - [Suspense](#suspense)
    - [atomWithSuspenseQuery](#atomwithsuspensequery-usage)
    - [atomWithSuspenseInfiniteQuery](#atomwithsuspenseinfinitequery-usage)
- [SSR Support](#ssr-support)
  - [Next.js App Router Example](#nextjs-app-router-example)
- [Error Handling](#error-handling)
- [Dev Tools](#devtools)
- [FAQ](#faq)
- [Migrate to v0.8.0](#migrate-to-v080)

### Support

jotai-tanstack-query currently supports [Jotai v2](https://jotai.org) and [TanStack Query v5](https://tanstack.com/query/v5).

### Install

```bash
npm i jotai jotai-tanstack-query @tanstack/react-query
```

### Usage

```jsx
import { QueryClient } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
import { QueryClientAtomProvider } from 'jotai-tanstack-query/react'

const queryClient = new QueryClient()

export const Root = () => {
  return (
    <QueryClientAtomProvider client={queryClient}>
      <App />
    </QueryClientAtomProvider>
  )
}

const todosAtom = atomWithQuery(() => ({
  queryKey: ['todos'],
  queryFn: fetchTodoList,
}))

const App = () => {
  const { data, isPending, isError } = useAtomValue(todosAtom)

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>

  return <div>{JSON.stringify(data)}</div>
}
```

### Incremental Adoption

You can incrementally adopt `jotai-tanstack-query` in your app. It's not an all or nothing solution. You just have to ensure you are using the [same QueryClient instance](#exported-provider).

```jsx
// TanStack/Query
const { data, isPending, isError } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodoList,
})

// jotai-tanstack-query
const todosAtom = atomWithQuery(() => ({
  queryKey: ['todos'],
}))

const { data, isPending, isError } = useAtomValue(todosAtom)
```

### Exported provider

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/jotaijs/jotai-tanstack-query/tree/main/examples/08_query_client_atom_provider)

`QueryClientAtomProvider` is a ready-to-use wrapper that combines Jotai Provider and TanStack Query QueryClientProvider.

```jsx
import { QueryClient } from '@tanstack/react-query'
import { QueryClientAtomProvider } from 'jotai-tanstack-query/react'

const queryClient = new QueryClient()

export const Root = () => {
  return (
    <QueryClientAtomProvider client={queryClient}>
      <App />
    </QueryClientAtomProvider>
  )
}
```

Yes, you can absolutely combine them yourself.

```js
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Provider } from 'jotai/react'
import { queryClientAtom } from 'jotai-tanstack-query'
import { useHydrateAtoms } from 'jotai/react/utils'

const queryClient = new QueryClient()

const HydrateAtoms = ({ children }) => {
  useHydrateAtoms([[queryClientAtom, queryClient]])
  return children
}

export const Root = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <Provider>
        <HydrateAtoms>
          <App />
        </HydrateAtoms>
      </Provider>
    </QueryClientProvider>
  )
}
```

### Exported functions

- `atomWithQuery` for [useQuery](https://tanstack.com/query/v5/docs/react/reference/useQuery)
- `atomWithQueries` for [useQueries](https://tanstack.com/query/v5/docs/react/reference/useQueries)
- `atomWithInfiniteQuery` for [useInfiniteQuery](https://tanstack.com/query/v5/docs/react/reference/useInfiniteQuery)
- `atomWithMutation` for [useMutation](https://tanstack.com/query/v5/docs/react/reference/useMutation)
- `atomWithSuspenseQuery` for [useSuspenseQuery](https://tanstack.com/query/v5/docs/react/reference/useSuspenseQuery)
- `atomWithSuspenseInfiniteQuery` for [useSuspenseInfiniteQuery](https://tanstack.com/query/v5/docs/react/reference/useSuspenseInfiniteQuery)
- `atomWithMutationState` for [useMutationState](https://tanstack.com/query/v5/docs/react/reference/useMutationState)

All functions follow the same signature.

```ts
const dataAtom = atomWithSomething(getOptions, getQueryClient)
```

The first `getOptions` parameter is a function that returns an input to the observer.
The second optional `getQueryClient` parameter is a function that return [QueryClient](https://tanstack.com/query/v5/docs/reference/QueryClient).

### atomWithQuery usage

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/jotaijs/jotai-tanstack-query/tree/main/examples/01_query)

`atomWithQuery` creates a new atom that implements a standard [`Query`](https://tanstack.com/query/v5/docs/react/guides/queries) from TanStack Query.

```jsx
import { atom, useAtom } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'

const idAtom = atom(1)
const userAtom = atomWithQuery((get) => ({
  queryKey: ['users', get(idAtom)],
  queryFn: async ({ queryKey: [, id] }) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    return res.json()
  },
}))

const UserData = () => {
  const [{ data, isPending, isError }] = useAtom(userAtom)

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>

  return <div>{JSON.stringify(data)}</div>
}
```

### atomWithQueries usage

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/jotaijs/jotai-tanstack-query/tree/main/examples/07_queries)

`atomWithQueries` creates a new atom that implements `Dynamic Parallel Queries` from TanStack Query. It allows you to run multiple queries concurrently and optionally combine their results. You can [read more about Dynamic Parallel Queries here](https://tanstack.com/query/v5/docs/framework/react/guides/parallel-queries#dynamic-parallel-queries-with-usequeries).

There are two ways to use `atomWithQueries`:

#### Basic usage - Returns an array of query atoms

```jsx
import { Atom, atom, useAtom } from 'jotai'
import { type AtomWithQueryResult, atomWithQueries } from 'jotai-tanstack-query'

const userIdsAtom = atom([1, 2, 3])

// Independent atom - encapsulates query logic
const userQueryAtomsAtom = atom((get) => {
  const userIds = get(userIdsAtom)
  return atomWithQueries({
    queries: userIds.map((id) => () => ({
      queryKey: ['user', id],
      queryFn: async ({ queryKey: [, userId] }) => {
        const res = await fetch(
          `https://jsonplaceholder.typicode.com/users/${userId}`
        )
        return res.json()
      },
    })),
  })
})

// Independent UI component
const UserData = ({ queryAtom }: { queryAtom: Atom<AtomWithQueryResult> }) => {
  const [{ data, isPending, isError }] = useAtom(queryAtom)

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>
  if (!data) return null

  return (
    <div>
      {data.name} - {data.email}
    </div>
  )
}

// Component only needs one useAtom call
const UsersData = () => {
  const [userQueryAtoms] = useAtom(userQueryAtomsAtom)
  return (
    <div>
      {userQueryAtoms.map((queryAtom, index) => (
        <UserData key={index} queryAtom={queryAtom} />
      ))}
    </div>
  )
}
```

#### Advanced usage - Combine multiple query results

```jsx
import { Atom, atom, useAtom } from 'jotai'
import { atomWithQueries } from 'jotai-tanstack-query'

const userIdsAtom = atom([1, 2, 3])

// Independent atom - encapsulates combined query logic
const combinedUsersDataAtom = atom((get) => {
  const userIds = get(userIdsAtom)
  return atomWithQueries({
    queries: userIds.map((id) => () => ({
      queryKey: ['user', id],
      queryFn: async ({ queryKey: [, userId] }) => {
        const res = await fetch(
          `https://jsonplaceholder.typicode.com/users/${userId}`
        )
        return res.json()
      },
    })),
    combine: (results) => ({
      data: results.map((result) => result.data),
      isPending: results.some((result) => result.isPending),
      isError: results.some((result) => result.isError),
    }),
  })
})

// Component only needs one useAtom call
const CombinedUsersData = () => {
  const [combinedUsersDataAtomValue] = useAtom(combinedUsersDataAtom)
  const [{ data, isPending, isError }] = useAtom(combinedUsersDataAtomValue)

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>

  return (
    <div>
      {data.map((user) => (
        <div key={user.id}>
          {user.name} - {user.email}
        </div>
      ))}
    </div>
  )
}
```

### atomWithInfiniteQuery usage

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/jotaijs/jotai-tanstack-query/tree/main/examples/03_infinite)

`atomWithInfiniteQuery` is very similar to `atomWithQuery`, however it is for an `InfiniteQuery`, which is used for data that is meant to be paginated. You can [read more about Infinite Queries here](https://tanstack.com/query/v5/docs/guides/infinite-queries).

> Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. React Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists.

A notable difference between a standard query atom is the additional option `getNextPageParam` and `getPreviousPageParam`, which is what you'll use to instruct the query on how to fetch any additional pages.

```jsx
import { atom, useAtom } from 'jotai'
import { atomWithInfiniteQuery } from 'jotai-tanstack-query'

const postsAtom = atomWithInfiniteQuery(() => ({
  queryKey: ['posts'],
  queryFn: async ({ pageParam }) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`)
    return res.json()
  },
  getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1,
  initialPageParam: 1,
}))

const Posts = () => {
  const [{ data, fetchNextPage, isPending, isError, isFetching }] =
    useAtom(postsAtom)

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>

  return (
    <>
      {data.pages.map((page, index) => (
        <div key={index}>
          {page.map((post: any) => (
            <div key={post.id}>{post.title}</div>
          ))}
        </div>
      ))}
      <button onClick={() => fetchNextPage()}>Next</button>
    </>
  )
}
```

### atomWithMutation usage

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/jotaijs/jotai-tanstack-query/tree/main/examples/05_mutation)

`atomWithMutation` creates a new atom that implements a standard [`Mutation`](https://tanstack.com/query/v5/docs/guides/mutations) from TanStack Query.

> Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects.

`atomWithMutation` supports all options from TanStack Query's [`useMutation`](https://tanstack.com/query/v5/docs/react/reference/useMutation), including:

- `mutationKey` - A unique key for the mutation
- `mutationFn` - The function that performs the mutation
- `onMutate` - Called before the mutation is executed (useful for optimistic updates)
- `onSuccess` - Called when the mutation succeeds
- `onError` - Called when the mutation fails
- `onSettled` - Called when the mutation is settled (either success or error)
- `retry` - Number of retry attempts
- `retryDelay` - Delay between retries
- `gcTime` - Time until inactive mutations are garbage collected
- And all other [MutationOptions](https://tanstack.com/query/v5/docs/react/reference/useMutation#options)

#### Basic usage

```tsx
import { useAtom } from 'jotai/react'
import { atomWithMutation } from 'jotai-tanstack-query'

const postAtom = atomWithMutation(() => ({
  mutationKey: ['posts'],
  mutationFn: async ({ title }: { title: string }) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
      method: 'POST',
      body: JSON.stringify({
        title,
        body: 'body',
        userId: 1,
      }),
      headers: {
        'Content-type': 'application/json; charset=UTF-8',
      },
    })
    const data = await res.json()
    return data
  },
}))

const Posts = () => {
  const [{ mutate, isPending, status }] = useAtom(postAtom)
  return (
    <div>
      <button onClick={() => mutate({ title: 'foo' })} disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
      <pre>{JSON.stringify(status, null, 2)}</pre>
    </div>
  )
}
```

#### Optimistic Updates

`atomWithMutation` fully supports optimistic updates through the `onMutate`, `onError`, and `onSettled` callbacks. This allows you to update the UI immediately before the server responds, and roll back if the mutation fails.

```tsx
import { Getter } from 'jotai'
import { useAtom } from 'jotai/react'
import {
  atomWithMutation,
  atomWithQuery,
  queryClientAtom,
} from 'jotai-tanstack-query'

interface Post {
  id: number
  title: string
  body: string
  userId: number
}

interface NewPost {
  title: string
}

interface OptimisticContext {
  previousPosts: Post[] | undefined
}

// Query to fetch posts list
const postsQueryAtom = atomWithQuery(() => ({
  queryKey: ['posts'],
  queryFn: async () => {
    const res = await fetch(
      'https://jsonplaceholder.typicode.com/posts?_limit=5'
    )
    return res.json() as Promise<Post[]>
  },
}))

// Mutation with optimistic updates
const postAtom = atomWithMutation<Post, NewPost, Error, OptimisticContext>(
  (get) => {
    const queryClient = get(queryClientAtom)
    return {
      mutationKey: ['addPost'],
      mutationFn: async ({ title }: NewPost) => {
        const res = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
          method: 'POST',
          body: JSON.stringify({
            title,
            body: 'body',
            userId: 1,
          }),
          headers: {
            'Content-type': 'application/json; charset=UTF-8',
          },
        })
        const data = await res.json()
        return data as Post
      },
      // When mutate is called:
      onMutate: async (newPost: NewPost) => {
        // Cancel any outgoing refetches
        // (so they don't overwrite our optimistic update)
        await queryClient.cancelQueries({ queryKey: ['posts'] })

        // Snapshot the previous value
        const previousPosts = queryClient.getQueryData<Post[]>(['posts'])

        // Optimistically update to the new value
        queryClient.setQueryData<Post[]>(['posts'], (old) => {
          const optimisticPost: Post = {
            id: Date.now(), // Temporary ID
            title: newPost.title,
            body: 'body',
            userId: 1,
          }
          return old ? [...old, optimisticPost] : [optimisticPost]
        })

        // Return a result with the snapshotted value
        return { previousPosts }
      },
      // If the mutation fails, use the result returned from onMutate to roll back
      onError: (
        _err: Error,
        _newPost: NewPost,
        onMutateResult: OptimisticContext | undefined
      ) => {
        if (onMutateResult?.previousPosts) {
          queryClient.setQueryData(['posts'], onMutateResult.previousPosts)
        }
      },
      // Always refetch after error or success:
      onSettled: (
        _data: Post | undefined,
        _error: Error | null,
        _variables: NewPost,
        _onMutateResult: OptimisticContext | undefined
      ) => {
        queryClient.invalidateQueries({ queryKey: ['posts'] })
      },
    }
  }
)

const PostsList = () => {
  const [{ data: posts, isPending }] = useAtom(postsQueryAtom)

  if (isPending) return <div>Loading posts...</div>

  return (
    <div>
      <h3>Posts:</h3>
      <ul>
        {posts?.map((post: Post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

const AddPost = () => {
  const [{ mutate, isPending }] = useAtom(postAtom)
  const [title, setTitle] = React.useState('')

  return (
    <div>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Enter post title"
      />
      <button
        onClick={() => {
          if (title) {
            mutate({ title })
            setTitle('')
          }
        }}
        disabled={isPending}>
        {isPending ? 'Adding...' : 'Add Post'}
      </button>
    </div>
  )
}
```

For more details on optimistic updates, see the [TanStack Query Optimistic Updates guide](https://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updates).

### atomWithMutationState usage

`atomWithMutationState` creates a new atom that gives you access to all mutations in the [`MutationCache`](https://tanstack.com/query/v5/docs/react/reference/useMutationState).

```jsx
const mutationStateAtom = atomWithMutationState((get) => ({
  filters: {
    mutationKey: ['posts'],
  },
}))
```

### Suspense

jotai-tanstack-query can also be used with React's Suspense.

### atomWithSuspenseQuery usage

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/jotaijs/jotai-tanstack-query/tree/main/examples/02_suspense)

```jsx
import { atom, useAtom } from 'jotai'
import { atomWithSuspenseQuery } from 'jotai-tanstack-query'

const idAtom = atom(1)
const userAtom = atomWithSuspenseQuery((get) => ({
  queryKey: ['users', get(idAtom)],
  queryFn: async ({ queryKey: [, id] }) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    return res.json()
  },
}))

const UserData = () => {
  const [{ data }] = useAtom(userAtom)

  return <div>{JSON.stringify(data)}</div>
}
```

### atomWithSuspenseInfiniteQuery usage

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/jotaijs/jotai-tanstack-query/tree/main/examples/04_infinite_suspense)

```jsx
import { atom, useAtom } from 'jotai'
import { atomWithSuspenseInfiniteQuery } from 'jotai-tanstack-query'

const postsAtom = atomWithSuspenseInfiniteQuery(() => ({
  queryKey: ['posts'],
  queryFn: async ({ pageParam }) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`)
    return res.json()
  },
  getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1,
  initialPageParam: 1,
}))

const Posts = () => {
  const [{ data, fetchNextPage, isPending, isError, isFetching }] =
    useAtom(postsAtom)

  return (
    <>
      {data.pages.map((page, index) => (
        <div key={index}>
          {page.map((post: any) => (
            <div key={post.id}>{post.title}</div>
          ))}
        </div>
      ))}
      <button onClick={() => fetchNextPage()}>Next</button>
    </>
  )
}
```

### SSR support

To understand if your application can benefit from React Query when also using Server Components, see the article [You Might Not Need React Query](https://tkdodo.eu/blog/you-might-not-need-react-query).

All atoms can be used within the context of a server side rendered app, such as a next.js app or Gatsby app. You can [use both options](https://tanstack.com/query/v5/docs/framework/react/guides/ssr) that React Query supports for use within SSR apps, [hydration](https://tanstack.com/query/v5/docs/framework/react/guides/ssr#using-the-hydration-apis) or [`initialData`](https://tanstack.com/query/v5/docs/framework/react/guides/ssr#get-started-fast-with-initialdata).

#### Next.js App Router Example

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/jotaijs/jotai-tanstack-query/tree/main/examples/11_nextjs_app_router)

### Error handling

Fetch error will be thrown and can be caught with ErrorBoundary.
Refetching may recover from a temporary error.

See [a working example](https://stackblitz.com/github/jotaijs/jotai-tanstack-query/tree/main/examples/09_error_boundary) to learn more.

### Devtools

In order to use the Devtools, you need to install it additionally.

```bash
$ npm i @tanstack/react-query-devtools --save-dev
```

All you have to do is put the `<ReactQueryDevtools />` within `<QueryClientAtomProvider />`.

```tsx
import { QueryClient, QueryCache } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { QueryClientAtomProvider } from 'jotai-tanstack-query/react'

const queryClient = new QueryClient()

export const Root = () => {
  return (
    <QueryClientAtomProvider client={queryClient}>
      <App />
      <ReactQueryDevtools />
    </QueryClientAtomProvider>
  )
}
```

## FAQ

### atomsWithQuery `queryKey` type is always `unknown`?

Explicitly declare the `get:Getter` to get proper type inference for `queryKey`.

```tsx
import { Getter } from 'jotai'

// ❌ Without explicit Getter type, queryKey type might be unknown
const userAtom = atomWithQuery((get) => ({
  queryKey: ['users', get(idAtom).toString()],
  queryFn: async ({ queryKey: [, id] }) => {
    // typeof id = unknown
  },
}))

// ✅ With explicit Getter type, queryKey gets proper type inference
const userAtom = atomWithQuery((get: Getter) => ({
  queryKey: ['users', get(idAtom).toString()],
  queryFn: async ({ queryKey: [, id] }) => {
    // typeof id = string
  },
}))
```

### Why “tracked properties” are not supported

TanStack Query provides a feature called [tracked properties](https://tanstack.com/query/v5/docs/framework/react/guides/render-optimizations#tracked-properties), which only triggers a re-render when a property that was actually accessed changes.

For example:

```tsx
const { data } = useQuery(...)
```

If only data is accessed, changes to isFetching or status will not cause a re-render.

However, atomWithQuery intentionally does **not** implement this behavior.

The atom approach is to create derived atoms rather than automatic tracking.

```js
const queryAtom = atomWithQuery(...)

const dataAtom = atom((get) => get(queryAtom).data)

const Component = () => {
  const data = useAtomValue(dataAtom)
  ...
}
```

The component only subscribes to data, changes to other properties (e.g. isFetching) won’t trigger re-renders


## Migrate to v0.8.0

### Change in atom signature

All atom signatures have changed to be more consistent with TanStack Query.
v0.8.0 returns only a single atom, instead of a tuple of atoms, and hence the name change from `atomsWithSomething` to`atomWithSomething`.

```diff

- const [dataAtom, statusAtom] = atomsWithSomething(getOptions, getQueryClient)
+ const dataAtom = atomWithSomething(getOptions, getQueryClient)

```

### Simplified Return Structure

In the previous version of `jotai-tanstack-query`, the query atoms `atomsWithQuery` and `atomsWithInfiniteQuery` returned a tuple of atoms: `[dataAtom, statusAtom]`. This design separated the data and its status into two different atoms.

#### atomWithQuery and atomWithInfiniteQuery

- `dataAtom` was used to access the actual data (`TData`).
- `statusAtom` provided the status object (`QueryObserverResult<TData, TError>`), which included additional attributes like `isPending`, `isError`, etc.

In v0.8.0, they have been replaced by `atomWithQuery` and `atomWithInfiniteQuery` to return only a single `dataAtom`. This `dataAtom` now directly provides the `QueryObserverResult<TData, TError>`, aligning it closely with the behavior of Tanstack Query's bindings.

To migrate to the new version, replace the separate `dataAtom` and `statusAtom` usage with the unified `dataAtom` that now contains both data and status information.

```diff
- const [dataAtom, statusAtom] = atomsWithQuery(/* ... */);
- const [data] = useAtom(dataAtom);
- const [status] = useAtom(statusAtom);

+ const dataAtom = atomWithQuery(/* ... */);
+ const [{ data, isPending, isError }] = useAtom(dataAtom);
```

#### atomWithMutation

Similar to `atomsWithQuery` and `atomsWithInfiniteQuery`, `atomWithMutation` also returns a single atom instead of a tuple of atoms. The return type of the atom value is `MutationObserverResult<TData, TError, TVariables, TContext>`.

```diff

- const [, postAtom] = atomsWithMutation(/* ... */);
- const [post, mutate] = useAtom(postAtom); // Accessing mutation status from post; and mutate() to execute the mutation

+ const postAtom = atomWithMutation(/* ... */);
+ const [{ data, error, mutate }] = useAtom(postAtom); // Accessing mutation result and mutate method from the same atom

```


================================================
FILE: __tests__/01_basic.spec.tsx
================================================
import {
  atomWithInfiniteQuery,
  atomWithMutation,
  atomWithMutationState,
  atomWithQuery,
  atomWithSuspenseInfiniteQuery,
  atomWithSuspenseQuery,
  queryClientAtom,
} from '../src/index'

describe('basic spec', () => {
  it('should export functions', () => {
    expect(queryClientAtom).toBeDefined()
    expect(atomWithQuery).toBeDefined()
    expect(atomWithInfiniteQuery).toBeDefined()
    expect(atomWithMutation).toBeDefined()
    expect(atomWithSuspenseQuery).toBeDefined()
    expect(atomWithSuspenseInfiniteQuery).toBeDefined()
    expect(atomWithMutationState).toBeDefined()
  })
})


================================================
FILE: __tests__/atomWithInfiniteQuery.spec.tsx
================================================
import React, { Component, StrictMode, Suspense } from 'react'
import type { ReactNode } from 'react'
import { fireEvent, render } from '@testing-library/react'
import { useAtom } from 'jotai/react'
import { atom } from 'jotai/vanilla'
import { vi } from 'vitest'
import { atomWithInfiniteQuery } from '../src/index'

let originalConsoleError: typeof console.error

beforeEach(() => {
  originalConsoleError = console.error
  console.error = vi.fn()
})
afterEach(() => {
  console.error = originalConsoleError
})

it('infinite query basic test', async () => {
  let resolve = () => {}
  type DataResponse = { response: { count: number } }
  const countAtom = atomWithInfiniteQuery<DataResponse>(() => ({
    initialPageParam: 1,
    getNextPageParam: (lastPage) => lastPage.response.count + 1,
    queryKey: ['countInfinite'],

    queryFn: async ({ pageParam }) => {
      await new Promise<void>((r) => (resolve = r))
      return { response: { count: pageParam as number } }
    },
  }))

  const Counter = () => {
    const [countData] = useAtom(countAtom)

    const { data, isPending, isError } = countData

    if (isPending) return <>loading</>
    if (isError) return <>error</>

    return (
      <>
        <div>page count: {data.pages.length}</div>
      </>
    )
  }

  const { findByText } = render(
    <StrictMode>
      <Counter />
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('page count: 1')
})

it('infinite query next page test', async () => {
  const mockFetch = vi.fn((response: { count: number }) => ({ response }))
  let resolve = () => {}
  const countAtom = atomWithInfiniteQuery<{ response: { count: number } }>(
    () => ({
      initialPageParam: 1,
      queryKey: ['nextPageAtom'],
      queryFn: async ({ pageParam }) => {
        await new Promise<void>((r) => (resolve = r))
        return mockFetch({ count: pageParam as number })
      },
      getNextPageParam: (lastPage) => {
        const {
          response: { count },
        } = lastPage
        return (count + 1).toString()
      },
      getPreviousPageParam: (lastPage) => {
        const {
          response: { count },
        } = lastPage
        return (count - 1).toString()
      },
    })
  )
  const Counter = () => {
    const [countData] = useAtom(countAtom)

    const { isPending, isError, data, fetchNextPage, fetchPreviousPage } =
      countData

    if (isPending) return <>loading</>
    if (isError) return <>error</>

    return (
      <>
        <div>page count: {data.pages.length}</div>
        <button onClick={() => fetchNextPage()}>next</button>
        <button onClick={() => fetchPreviousPage()}>prev</button>
      </>
    )
  }

  const { findByText, getByText } = render(
    <>
      <Counter />
    </>
  )

  await findByText('loading')
  resolve()
  await findByText('page count: 1')
  expect(mockFetch).toBeCalledTimes(1)

  fireEvent.click(getByText('next'))
  resolve()
  await findByText('page count: 2')
  expect(mockFetch).toBeCalledTimes(2)

  fireEvent.click(getByText('prev'))
  resolve()
  await findByText('page count: 3')
  expect(mockFetch).toBeCalledTimes(3)
})

it('infinite query with enabled', async () => {
  const slugAtom = atom<string | null>(null)

  let resolve = () => {}
  type DataResponse = {
    response: {
      slug: string
      currentPage: number
    }
  }
  const slugQueryAtom = atomWithInfiniteQuery<DataResponse>((get) => {
    const slug = get(slugAtom)
    return {
      initialPageParam: 1,
      getNextPageParam: (lastPage) => lastPage.response.currentPage + 1,
      enabled: !!slug,
      queryKey: ['disabled_until_value', slug],
      queryFn: async ({ pageParam }) => {
        await new Promise<void>((r) => (resolve = r))
        return {
          response: { slug: `hello-${slug}`, currentPage: pageParam as number },
        }
      },
    }
  })

  const Slug = () => {
    const [slugQueryData] = useAtom(slugQueryAtom)
    const { data, isPending, isError, fetchStatus } = slugQueryData

    if (isPending && fetchStatus === 'idle') return <div>not enabled</div>

    if (isPending) return <>loading</>
    if (isError) return <>error</>

    return <div>slug: {data.pages[0]?.response?.slug}</div>
  }

  const Parent = () => {
    const [, setSlug] = useAtom(slugAtom)
    return (
      <div>
        <button
          onClick={() => {
            setSlug('world')
          }}>
          set slug
        </button>
        <Slug />
      </div>
    )
  }

  const { getByText, findByText } = render(
    <StrictMode>
      <Suspense fallback="loading">
        <Parent />
      </Suspense>
    </StrictMode>
  )

  await findByText('not enabled')

  fireEvent.click(getByText('set slug'))
  await findByText('loading')
  resolve()
  await findByText('slug: hello-world')
})

it('infinite query with enabled 2', async () => {
  const enabledAtom = atom<boolean>(true)
  const slugAtom = atom<string | null>('first')
  type DataResponse = {
    response: {
      slug: string
      currentPage: number
    }
  }
  let resolve = () => {}
  const slugQueryAtom = atomWithInfiniteQuery<DataResponse>((get) => {
    const slug = get(slugAtom)
    const isEnabled = get(enabledAtom)
    return {
      getNextPageParam: (lastPage) => lastPage.response.currentPage + 1,
      initialPageParam: 1,
      enabled: isEnabled,
      queryKey: ['enabled_toggle'],
      queryFn: async ({ pageParam }) => {
        await new Promise<void>((r) => (resolve = r))
        return {
          response: { slug: `hello-${slug}`, currentPage: pageParam as number },
        }
      },
    }
  })

  const Slug = () => {
    const [slugQueryData] = useAtom(slugQueryAtom)
    const { data, isPending, isError, fetchStatus } = slugQueryData

    if (isPending && fetchStatus === 'idle') return <div>not enabled</div>

    if (isPending) return <>loading</>
    if (isError) return <>error</>

    return <div>slug: {data.pages[0]?.response.slug}</div>
  }

  const Parent = () => {
    const [, setSlug] = useAtom(slugAtom)
    const [, setEnabled] = useAtom(enabledAtom)
    return (
      <div>
        <button
          onClick={() => {
            setSlug('world')
          }}>
          set slug
        </button>
        <button
          onClick={() => {
            setEnabled(true)
          }}>
          set enabled
        </button>
        <button
          onClick={() => {
            setEnabled(false)
          }}>
          set disabled
        </button>
        <Slug />
      </div>
    )
  }

  const { getByText, findByText } = render(
    <StrictMode>
      <Suspense fallback="loading">
        <Parent />
      </Suspense>
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('slug: hello-first')
  fireEvent.click(getByText('set disabled'))
  fireEvent.click(getByText('set slug'))

  await findByText('slug: hello-first')

  fireEvent.click(getByText('set enabled'))
  resolve()
  await findByText('slug: hello-world')
})

describe('error handling', () => {
  class ErrorBoundary extends Component<
    { message?: string; retry?: () => void; children: ReactNode },
    { hasError: boolean }
  > {
    constructor(props: { message?: string; children: ReactNode }) {
      super(props)
      this.state = { hasError: false }
    }
    static getDerivedStateFromError() {
      return { hasError: true }
    }
    render() {
      return this.state.hasError ? (
        <div>
          {this.props.message || 'errored'}
          {this.props.retry && (
            <button
              onClick={() => {
                this.props.retry?.()
                this.setState({ hasError: false })
              }}>
              retry
            </button>
          )}
        </div>
      ) : (
        this.props.children
      )
    }
  }

  it('can catch error in error boundary', async () => {
    let resolve = () => {}
    const countAtom = atomWithInfiniteQuery(() => ({
      initialPageParam: 1,
      getNextPageParam: (lastPage) => lastPage.response.count + 1,
      queryKey: ['error test', 'count1Infinite'],
      retry: false,
      queryFn: async (): Promise<{ response: { count: number } }> => {
        await new Promise<void>((r) => (resolve = r))
        throw new Error('fetch error')
      },
      throwOnError: true,
    }))
    const Counter = () => {
      const [{ data, isPending }] = useAtom(countAtom)

      if (isPending) return <>loading</>

      const pages = data?.pages

      return (
        <>
          <div>count: {pages?.[0]?.response.count}</div>
        </>
      )
    }

    const { findByText } = render(
      <StrictMode>
        <ErrorBoundary>
          <Suspense fallback="loading">
            <Counter />
          </Suspense>
        </ErrorBoundary>
      </StrictMode>
    )

    await findByText('loading')
    resolve()
    await findByText('errored')
  })
})


================================================
FILE: __tests__/atomWithMutation.spec.tsx
================================================
import React, { useState } from 'react'
import { fireEvent, render } from '@testing-library/react'
import { useAtom } from 'jotai/react'
import { atomWithMutation } from '../src/index'

it('atomWithMutation should be refreshed on unmount (#2060)', async () => {
  let resolve: (() => void) | undefined
  const mutateAtom = atomWithMutation<number, number>(() => ({
    mutationKey: ['test-atom'],
    mutationFn: async (a) => {
      await new Promise<void>((r) => {
        resolve = r
      })
      return a
    },
  }))

  function App() {
    const [mount, setMount] = useState<boolean>(true)
    return (
      <div>
        <button onClick={() => setMount(false)}>unmount</button>
        <button onClick={() => setMount(true)}>mount</button>
        {mount && <TestView />}
      </div>
    )
  }

  function TestView() {
    const [{ mutate, isPending, status }] = useAtom(mutateAtom)
    return (
      <div>
        <p>status: {status}</p>
        <button disabled={isPending} onClick={() => mutate(1)}>
          mutate
        </button>
      </div>
    )
  }

  const { findByText, getByText } = render(<App />)

  await findByText('status: idle')

  fireEvent.click(getByText('mutate'))
  await findByText('status: pending')
  resolve?.()
  await findByText('status: success')

  fireEvent.click(getByText('unmount'))
  fireEvent.click(getByText('mount'))
  await findByText('status: idle')
  resolve?.()
})


================================================
FILE: __tests__/atomWithMutationState.spec.tsx
================================================
import React from 'react'
import { QueryClient } from '@tanstack/query-core'
import { fireEvent, render } from '@testing-library/react'
import { Provider, useAtom } from 'jotai'
import { atomWithMutation, atomWithMutationState } from '../src'

it('atomWithMutationState multiple', async () => {
  const client = new QueryClient()
  let resolve1: (() => void) | undefined
  const mutateAtom1 = atomWithMutation<number, number>(
    () => ({
      mutationKey: ['test-atom'],
      mutationFn: async (a) => {
        await new Promise<void>((r) => {
          resolve1 = r
        })
        return a
      },
    }),
    () => client
  )
  let resolve2: (() => void) | undefined
  const mutateAtom2 = atomWithMutation<number, number>(
    () => ({
      mutationKey: ['test-atom'],
      mutationFn: async (a) => {
        await new Promise<void>((r) => {
          resolve2 = r
        })
        return a
      },
    }),
    () => client
  )

  const pendingMutationStateAtom = atomWithMutationState(
    () => ({ filters: { mutationKey: ['test-atom'], status: 'pending' } }),
    () => client
  )

  const allMutationStatesAtom = atomWithMutationState(
    () => ({ filters: { mutationKey: ['test-atom'] } }),
    () => client
  )

  function App() {
    const [{ mutate: mutate1 }] = useAtom(mutateAtom1)
    const [{ mutate: mutate2 }] = useAtom(mutateAtom2)
    const [pendingMutationState] = useAtom(pendingMutationStateAtom)
    const [allMutationStates] = useAtom(allMutationStatesAtom)

    return (
      <div>
        <p>all: {allMutationStates.length}</p>
        <p>pending: {pendingMutationState.length}</p>
        <button
          onClick={() => {
            mutate1(1)
            mutate2(2)
          }}>
          mutate
        </button>
      </div>
    )
  }

  const { findByText, getByText } = render(
    <Provider>
      <App />
    </Provider>
  )

  await findByText('all: 0')
  await findByText('pending: 0')
  fireEvent.click(getByText('mutate'))
  await findByText('all: 2')
  await findByText('pending: 2')
  resolve1?.()
  await findByText('all: 2')
  await findByText('pending: 1')
  resolve2?.()
  await findByText('all: 2')
  await findByText('pending: 0')
})


================================================
FILE: __tests__/atomWithQuery.spec.tsx
================================================
import React, { StrictMode, Suspense, useState } from 'react'
import { QueryClient } from '@tanstack/query-core'
import { fireEvent, render } from '@testing-library/react'
import { Getter, atom, useAtom, useSetAtom } from 'jotai'
import { unwrap } from 'jotai/utils'
import { ErrorBoundary } from 'react-error-boundary'
import { vi } from 'vitest'
import { atomWithQuery } from '../src'

let originalConsoleError: typeof console.error

beforeEach(() => {
  originalConsoleError = console.error
  console.error = vi.fn()
})
afterEach(() => {
  console.error = originalConsoleError
})

it('query basic test', async () => {
  let resolve = () => {}
  const countAtom = atomWithQuery(() => ({
    queryKey: ['test1'],
    queryFn: async () => {
      await new Promise<void>((r) => (resolve = r))
      return { response: { count: 0 } }
    },
  }))
  const Counter = () => {
    const [countData] = useAtom(countAtom)
    const { data, isPending, isError } = countData

    if (isPending) {
      return <>loading</>
    }

    if (isError) {
      return <>errored</>
    }

    return (
      <>
        <div>count: {data.response.count}</div>
      </>
    )
  }

  const { findByText } = render(
    <StrictMode>
      <Counter />
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('count: 0')
})

it('async query basic test', async () => {
  const fn = vi.fn(() => Promise.resolve(2))
  const queryFn = vi.fn((id) => {
    return Promise.resolve({ response: { id } })
  })

  const userIdAtom = atom(async () => {
    return await fn()
  })

  const userAtom = atomWithQuery((get) => {
    const userId = get(unwrap(userIdAtom))

    return {
      queryKey: ['userId', userId],
      queryFn: async ({ queryKey: [, id] }) => {
        const res = await queryFn(id)
        return res
      },
      enabled: !!userId,
    }
  })
  const User = () => {
    const [userData] = useAtom(userAtom)
    const { data, isPending, isError } = userData

    if (isPending) return <>loading</>
    if (isError) return <>errored</>

    return (
      <>
        <div>id: {data.response.id}</div>
      </>
    )
  }
  const { findByText } = render(
    <StrictMode>
      <Suspense fallback="loading">
        <User />
      </Suspense>
    </StrictMode>
  )

  await findByText('loading')
  await findByText('id: 2')
  expect(queryFn).toHaveBeenCalledTimes(1)
})

it('query refetch', async () => {
  let count = 0
  const mockFetch = vi.fn((response: { count: number }) => ({
    response,
  }))
  let resolve = () => {}
  const countAtom = atomWithQuery(() => ({
    queryKey: ['test3'],
    queryFn: async () => {
      await new Promise<void>((r) => (resolve = r))
      const response = mockFetch({ count })
      ++count
      return response
    },
  }))
  const Counter = () => {
    const [{ data, isPending, isError, refetch }] = useAtom(countAtom)

    if (isPending) {
      return <>loading</>
    }

    if (isError) {
      return <>errored</>
    }

    return (
      <>
        <div>count: {data?.response.count}</div>
        <button onClick={() => refetch()}>refetch</button>
      </>
    )
  }

  const { findByText, getByText } = render(
    <StrictMode>
      <Counter />
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('count: 0')
  expect(mockFetch).toBeCalledTimes(1)

  fireEvent.click(getByText('refetch'))
  await expect(() => findByText('loading')).rejects.toThrow() //refetch implementation in tanstack doesn't trigger loading state
  resolve()
  await findByText('count: 1')
  expect(mockFetch).toBeCalledTimes(2)
})

it('query with enabled', async () => {
  const slugAtom = atom<string | null>(null)
  const mockFetch = vi.fn((response) => ({ response }))
  let resolve = () => {}
  const slugQueryAtom = atomWithQuery((get) => {
    const slug = get(slugAtom)
    return {
      enabled: !!slug,
      queryKey: ['disabled_until_value', slug],
      queryFn: async () => {
        await new Promise<void>((r) => (resolve = r))
        return mockFetch({ slug: `hello-${slug}` })
      },
    }
  })

  const Slug = () => {
    const [slugQueryData] = useAtom(slugQueryAtom)

    const { data, isPending, isError, status, fetchStatus } = slugQueryData

    //ref: https://tanstack.com/query/v4/docs/react/guides/dependent-queries
    if (status === 'pending' && fetchStatus === 'idle') {
      return <div>not enabled</div>
    }

    if (isPending) {
      return <>loading</>
    }

    if (isError) {
      return <>errored</>
    }

    return <div>slug: {data.response.slug}</div>
  }

  const Parent = () => {
    const [, setSlug] = useAtom(slugAtom)
    return (
      <div>
        <button
          onClick={() => {
            setSlug('world')
          }}>
          set slug
        </button>
        <Slug />
      </div>
    )
  }

  const { getByText, findByText } = render(
    <StrictMode>
      <Parent />
    </StrictMode>
  )

  await findByText('not enabled')
  expect(mockFetch).toHaveBeenCalledTimes(0)

  fireEvent.click(getByText('set slug'))
  await findByText('loading')
  resolve()
  await findByText('slug: hello-world')
  expect(mockFetch).toHaveBeenCalledTimes(1)
})

it('query with enabled 2', async () => {
  const mockFetch = vi.fn((response) => ({ response }))
  const enabledAtom = atom<boolean>(true)
  const slugAtom = atom<string | null>('first')
  let resolve = () => {}

  const slugQueryAtom = atomWithQuery((get: Getter) => {
    const slug = get(slugAtom)
    const enabled = get(enabledAtom)
    return {
      enabled: enabled,
      queryKey: ['enabled_toggle'],
      queryFn: async () => {
        await new Promise<void>((r) => (resolve = r))

        return mockFetch({ slug: `hello-${slug}` })
      },
    }
  })

  const Slug = () => {
    const [slugQueryAtomData] = useAtom(slugQueryAtom)
    const { data, isError, isPending, status, fetchStatus } = slugQueryAtomData

    if (status === 'pending' && fetchStatus === 'idle') {
      return <div>not enabled</div>
    }

    if (isPending) {
      return <>loading</>
    }

    if (isError) {
      return <>errored</>
    }
    return <div>slug: {data.response.slug}</div>
  }

  const Parent = () => {
    const [, setSlug] = useAtom(slugAtom)
    const [, setEnabled] = useAtom(enabledAtom)
    return (
      <div>
        <button
          onClick={() => {
            setSlug('world')
          }}>
          set slug
        </button>
        <button
          onClick={() => {
            setEnabled(true)
          }}>
          set enabled
        </button>
        <button
          onClick={() => {
            setEnabled(false)
          }}>
          set disabled
        </button>
        <Slug />
      </div>
    )
  }

  const { getByText, findByText } = render(
    <StrictMode>
      <Parent />
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('slug: hello-first')
  expect(mockFetch).toHaveBeenCalledTimes(1)

  fireEvent.click(getByText('set disabled'))
  fireEvent.click(getByText('set slug'))

  await findByText('slug: hello-first')
  expect(mockFetch).toHaveBeenCalledTimes(1)

  fireEvent.click(getByText('set enabled'))

  await expect(() => findByText('loading')).rejects.toThrow() //refetch implementation in tanstack doesn't trigger loading state
  resolve()
  await findByText('slug: hello-world')
  expect(mockFetch).toHaveBeenCalledTimes(2)
})

it('query with enabled (#500)', async () => {
  const enabledAtom = atom(true)
  let resolve = () => {}
  const countAtom = atomWithQuery((get) => {
    const enabled = get(enabledAtom)
    return {
      enabled,
      queryKey: ['count_500_issue'],
      queryFn: async () => {
        await new Promise<void>((r) => (resolve = r))
        return { response: { count: 1 } }
      },
    }
  })

  const Counter = () => {
    const [countData] = useAtom(countAtom)

    const { data, isPending, isError } = countData

    if (isPending) {
      return <>loading</>
    }

    if (isError) {
      return <>errored</>
    }

    return <div>count: {data.response.count}</div>
  }

  const Parent = () => {
    const [showChildren, setShowChildren] = useState(true)
    const [, setEnabled] = useAtom(enabledAtom)
    return (
      <div>
        <button
          onClick={() => {
            setShowChildren((x) => !x)
            setEnabled((x) => !x)
          }}>
          toggle
        </button>
        {showChildren ? <Counter /> : <div>hidden</div>}
      </div>
    )
  }

  const { getByText, findByText } = render(
    <StrictMode>
      <Suspense fallback="loading">
        <Parent />
      </Suspense>
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('count: 1')

  fireEvent.click(getByText('toggle'))
  resolve()
  await findByText('hidden')

  fireEvent.click(getByText('toggle'))
  resolve()
  await findByText('count: 1')
})

it('query with initialData test', async () => {
  const mockFetch = vi.fn((response: { count: number }) => ({
    response,
  }))

  let resolve = () => {}

  const countAtom = atomWithQuery(() => ({
    queryKey: ['initialData_count1'],
    queryFn: async () => {
      await new Promise<void>((r) => (resolve = r))
      return mockFetch({ count: 10 })
    },
    initialData: { response: { count: 0 } },
  }))
  const Counter = () => {
    const [
      {
        data: {
          response: { count },
        },
      },
    ] = useAtom(countAtom)

    return (
      <>
        <div>count: {count}</div>
      </>
    )
  }

  const { findByText } = render(
    <StrictMode>
      <Counter />
    </StrictMode>
  )

  // NOTE: the atom is never loading
  await expect(() => findByText('loading')).rejects.toThrow()
  await findByText('count: 0')
  resolve()
  await findByText('count: 10')
  expect(mockFetch).toHaveBeenCalledTimes(1)
})

it('query dependency test', async () => {
  const baseCountAtom = atom(0)
  const incrementAtom = atom(null, (_get, set) =>
    set(baseCountAtom, (c) => c + 1)
  )
  let resolve = () => {}
  const countAtom = atomWithQuery((get) => ({
    queryKey: ['count_with_dependency', get(baseCountAtom)],
    queryFn: async () => {
      await new Promise<void>((r) => (resolve = r))
      return { response: { count: get(baseCountAtom) } }
    },
  }))

  const Counter = () => {
    const [countData] = useAtom(countAtom)
    const { data, isPending, isError } = countData

    if (isPending) {
      return <>loading</>
    }

    if (isError) {
      return <>errored</>
    }

    return (
      <>
        <div>count: {data.response.count}</div>
      </>
    )
  }

  const Controls = () => {
    const [, increment] = useAtom(incrementAtom)
    return <button onClick={increment}>increment</button>
  }

  const { getByText, findByText } = render(
    <StrictMode>
      <Counter />
      <Controls />
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('count: 0')

  fireEvent.click(getByText('increment'))
  await findByText('loading')
  resolve()
  await findByText('count: 1')
})

it('query expected QueryCache test', async () => {
  const queryClient = new QueryClient()
  let resolve = () => {}
  const countAtom = atomWithQuery(
    () => ({
      queryKey: ['count6'],
      queryFn: async () => {
        await new Promise<void>((r) => (resolve = r))
        return { response: { count: 0 } }
      },
    }),
    () => queryClient
  )
  const Counter = () => {
    const [countData] = useAtom(countAtom)

    const { data, isPending, isError } = countData

    if (isPending) {
      return <>loading</>
    }

    if (isError) {
      return <>errored</>
    }

    return (
      <>
        <div>count: {data.response.count}</div>
      </>
    )
  }

  const { findByText } = render(
    <StrictMode>
      <Counter />
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('count: 0')
  expect(queryClient.getQueryCache().getAll().length).toBe(1)
})

describe('error handling', () => {
  it('can catch error in error boundary', async () => {
    let resolve = () => {}
    const countAtom = atomWithQuery(() => ({
      queryKey: ['catch'],
      retry: false,
      queryFn: async (): Promise<{ response: { count: number } }> => {
        await new Promise<void>((r) => (resolve = r))
        throw new Error('fetch error')
      },
      throwOnError: true,
    }))
    const Counter = () => {
      const [countData] = useAtom(countAtom)

      if ('data' in countData) {
        if (countData.isPending) {
          return <>loading</>
        }

        return <div>count: {countData.data?.response.count}</div>
      }

      return null
    }

    const { findByText } = render(
      <StrictMode>
        <ErrorBoundary fallback={<>errored</>}>
          <Counter />
        </ErrorBoundary>
      </StrictMode>
    )

    await findByText('loading')
    resolve()
    await findByText('errored')
  })

  it('can recover from error', async () => {
    let count = -1
    let willThrowError = false
    let resolve = () => {}
    const countAtom = atomWithQuery(() => ({
      queryKey: ['error test', 'count2'],
      retry: false,
      queryFn: async () => {
        willThrowError = !willThrowError
        ++count
        await new Promise<void>((r) => (resolve = r))
        if (willThrowError) {
          throw new Error('fetch error')
        }
        return { response: { count } }
      },
      throwOnError: true,
    }))
    const Counter = () => {
      const [countData] = useAtom(countAtom)
      if (countData.isFetching) return <>loading</>
      return (
        <>
          <div>count: {countData.data?.response.count}</div>
          <button onClick={() => countData.refetch()}>refetch</button>
        </>
      )
    }

    const App = () => {
      return (
        <ErrorBoundary
          FallbackComponent={({ resetErrorBoundary }) => {
            return (
              <>
                <h1>errored</h1>
                <button onClick={() => resetErrorBoundary()}>retry</button>
              </>
            )
          }}>
          <Counter />
        </ErrorBoundary>
      )
    }

    const { findByText, getByText } = render(
      <>
        <App />
      </>
    )

    await findByText('loading')
    resolve()
    await findByText('errored')

    fireEvent.click(getByText('retry'))
    await findByText('loading')
    resolve()
    await findByText('count: 1')

    fireEvent.click(getByText('refetch'))
    await findByText('loading')
    resolve()
    await findByText('errored')

    fireEvent.click(getByText('retry'))
    await findByText('loading')
    resolve()
    await findByText('count: 3')
  })
})

// Test for bug described here:
// https://github.com/jotaijs/jotai-tanstack-query/issues/34
// Note: If error handling tests run after this test, they are failing. Not sure why.
it('renews the result when the query changes and a non stale cache is available', async () => {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { staleTime: 5 * 60 * 1000 } },
  })
  queryClient.setQueryData(['currentCount', 2], 2)

  const currentCountAtom = atom(1)

  const countAtom = atomWithQuery(
    (get) => {
      const currentCount = get(currentCountAtom)
      return {
        queryKey: ['currentCount', currentCount],
        queryFn: () => currentCount,
      }
    },
    () => queryClient
  )

  const Counter = () => {
    const setCurrentCount = useSetAtom(currentCountAtom)
    const [countData] = useAtom(countAtom)

    const { data, isPending, isError } = countData

    if (isPending) {
      return <>loading</>
    }

    if (isError) {
      return <>errored</>
    }

    return (
      <>
        <button onClick={() => setCurrentCount(2)}>Set count to 2</button>
        <div>count: {data}</div>
      </>
    )
  }

  const { findByText } = render(
    <StrictMode>
      <Counter />
    </StrictMode>
  )
  await findByText('loading')
  await findByText('count: 1')
  fireEvent.click(await findByText('Set count to 2'))
  await expect(() => findByText('loading')).rejects.toThrow()
  await findByText('count: 2')
})

// https://github.com/jotaijs/jotai-tanstack-query/pull/40
it(`ensure that setQueryData for an inactive query updates its atom state`, async () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        refetchOnMount: false,
      },
    },
  })

  const extraKey = 'uniqueKey'
  const pageAtom = atom(1)

  const queryFn = vi.fn(() => {
    return Promise.resolve('John Doe')
  })

  const userAtom = atomWithQuery(
    () => {
      return {
        queryKey: [extraKey],
        queryFn: async () => {
          const name = await queryFn()
          return { response: { name } }
        },
      }
    },
    () => queryClient
  )

  const User = () => {
    const [{ data, isPending }] = useAtom(userAtom)

    if (isPending) return <>loading</>

    return <>Name: {data?.response.name}</>
  }

  const Controls = () => {
    const [, setPage] = useAtom(pageAtom)
    return (
      <>
        <button onClick={() => setPage(1)}>Set page 1</button>
        <button onClick={() => setPage(2)}>Set page 2</button>
      </>
    )
  }

  const App = () => {
    const [page] = useAtom(pageAtom)
    return (
      <>
        {page === 1 && <User />}
        <Controls />
      </>
    )
  }

  const { findByText } = render(
    <StrictMode>
      <App />
    </StrictMode>
  )

  await findByText('loading')
  await findByText('Name: John Doe')
  fireEvent.click(await findByText('Set page 2'))
  queryClient.setQueryData([extraKey], { response: { name: 'Alex Smith' } })
  fireEvent.click(await findByText('Set page 1'))
  await expect(() => findByText('loading')).rejects.toThrow()
  await findByText('Name: Alex Smith')
})


================================================
FILE: __tests__/atomWithSuspenseInfiniteQuery.spec.tsx
================================================
import React, { StrictMode, Suspense } from 'react'
import { fireEvent, render } from '@testing-library/react'
import { useAtom } from 'jotai'
import { atomWithSuspenseInfiniteQuery } from '../src'

it('suspense basic, suspends', async () => {
  let resolve = () => {}
  type DataResponse = {
    response: {
      count: number
    }
  }
  const countAtom = atomWithSuspenseInfiniteQuery<DataResponse>(() => ({
    getNextPageParam: (lastPage) => {
      const nextPageParam = lastPage.response.count + 1
      return nextPageParam
    },
    initialPageParam: 1,
    queryKey: ['test1'],
    queryFn: async ({ pageParam }) => {
      await new Promise<void>((r) => (resolve = r))
      return { response: { count: pageParam as number } }
    },
  }))
  const Counter = () => {
    const [countData] = useAtom(countAtom)
    const { data, fetchNextPage } = countData
    return (
      <>
        <div>count: {data?.pages?.[data.pages.length - 1]?.response.count}</div>
        <button
          onClick={() => {
            fetchNextPage()
          }}>
          fetchNextPage
        </button>
      </>
    )
  }

  const { findByText, getByText } = render(
    <StrictMode>
      <Suspense fallback="loading">
        <Counter />
      </Suspense>
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('count: 1')

  fireEvent.click(getByText('fetchNextPage'))
  await expect(() => findByText('loading')).rejects.toThrow() //refetch implementation in tanstack doesn't trigger loading state
  resolve()
  await findByText('count: 2')
})


================================================
FILE: __tests__/atomWithSuspenseQuery.spec.tsx
================================================
import React, { StrictMode, Suspense } from 'react'
import { QueryClient } from '@tanstack/query-core'
import { fireEvent, render } from '@testing-library/react'
import { Provider, atom, useAtom, useSetAtom } from 'jotai'
import { ErrorBoundary } from 'react-error-boundary'
import { vi } from 'vitest'
import { atomWithSuspenseQuery } from '../src'

let originalConsoleError: typeof console.error

beforeEach(() => {
  originalConsoleError = console.error
  console.error = vi.fn()
})
afterEach(() => {
  console.error = originalConsoleError
})

it('suspense basic, suspends', async () => {
  let resolve = () => {}
  const countAtom = atomWithSuspenseQuery(() => ({
    queryKey: ['test1', 'suspends'],
    queryFn: async () => {
      await new Promise<void>((r) => (resolve = r))
      return { response: { count: 0 } }
    },
  }))
  const Counter = () => {
    const [{ data }] = useAtom(countAtom)
    return (
      <>
        <div>count: {data.response.count}</div>
      </>
    )
  }

  const { findByText } = render(
    <StrictMode>
      <Suspense fallback="Loading...">
        <Counter />
      </Suspense>
    </StrictMode>
  )

  await findByText('Loading...')
  resolve()
  await findByText('count: 0')
})

it('query refetch', async () => {
  const mockFetch = vi.fn((response: { message: string }) => ({
    response,
  }))
  let resolve = () => {}
  const greetingAtom = atomWithSuspenseQuery(() => ({
    queryKey: ['test3'],
    queryFn: async () => {
      await new Promise<void>((r) => (resolve = r))
      const response = mockFetch({ message: 'helloWorld' })
      return response
    },
  }))
  const Greeting = () => {
    const [{ data, refetch }] = useAtom(greetingAtom)

    return (
      <>
        <div>message: {data?.response.message}</div>
        <button onClick={() => refetch?.()}>refetch</button>
      </>
    )
  }

  const { findByText, getByText } = render(
    <StrictMode>
      <Suspense fallback="loading">
        <Greeting />
      </Suspense>
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('message: helloWorld')
  expect(mockFetch).toBeCalledTimes(1)

  fireEvent.click(getByText('refetch'))
  await expect(() => findByText('loading')).rejects.toThrow() //refetch implementation in tanstack doesn't trigger loading state
  resolve()
  await findByText('message: helloWorld')
  expect(mockFetch).toBeCalledTimes(2) //this ensures we are actually running the query function again
})

describe('intialData test', () => {
  it('query with initialData test', async () => {
    const mockFetch = vi.fn((response) => ({ response }))
    let resolve = () => {}

    const countAtom = atomWithSuspenseQuery(() => ({
      queryKey: ['initialData_count1'],
      queryFn: async () => {
        await new Promise<void>((r) => (resolve = r))
        return mockFetch({ count: 10 })
      },
      initialData: { response: { count: 0 } },
      staleTime: 0,
    }))
    const Counter = () => {
      const [countData] = useAtom(countAtom)
      const { data, isError } = countData

      if (isError) {
        return <>errored</>
      }

      const count = data?.response.count
      return (
        <>
          <div>count: {count}</div>
        </>
      )
    }

    const { findByText } = render(
      <StrictMode>
        <Suspense fallback="loading">
          <Counter />
        </Suspense>
      </StrictMode>
    )

    // NOTE: the atom is never loading
    await expect(() => findByText('loading')).rejects.toThrow()
    await findByText('count: 0')
    resolve()
    await findByText('count: 10')
    expect(mockFetch).toHaveBeenCalledTimes(1)
  })

  it('query with initialData test with dependency', async () => {
    const mockFetch = vi.fn((response) => ({ response }))
    let resolve = () => {}
    const numberAtom = atom(10)
    const countAtom = atomWithSuspenseQuery((get) => ({
      queryKey: ['initialData_count1', get(numberAtom)],
      queryFn: async ({ queryKey: [, myNumber] }) => {
        await new Promise<void>((r) => (resolve = r))
        return mockFetch({ count: myNumber })
      },
      initialData: { response: { count: 0 } },
      staleTime: 0,
    }))
    const Counter = () => {
      const [countData] = useAtom(countAtom)
      const { data, isError } = countData
      if (isError) {
        return <>errored</>
      }
      const count = data?.response.count
      return (
        <>
          <div>count: {count}</div>
        </>
      )
    }

    const Increment = () => {
      const setNumber = useSetAtom(numberAtom)
      return <button onClick={() => setNumber((n) => n + 1)}>increment</button>
    }
    const { findByText } = render(
      <StrictMode>
        <Suspense fallback="loading">
          <Counter />
        </Suspense>
        <Increment />
      </StrictMode>
    )
    // NOTE: the atom is never loading
    await expect(() => findByText('loading')).rejects.toThrow()
    await findByText('count: 0')
    resolve()
    await findByText('count: 10')
    expect(mockFetch).toHaveBeenCalledTimes(1)
    await findByText('increment')
    fireEvent.click(await findByText('increment'))
    await findByText('count: 0')
    resolve()
    await findByText('count: 11')
    expect(mockFetch).toHaveBeenCalledTimes(2)
  })
})

it('query dependency test', async () => {
  const baseCountAtom = atom(0)
  const incrementAtom = atom(null, (_get, set) =>
    set(baseCountAtom, (c) => c + 1)
  )
  let resolve = () => {}
  const countAtom = atomWithSuspenseQuery((get) => ({
    queryKey: ['count_with_dependency', get(baseCountAtom)],
    queryFn: async () => {
      await new Promise<void>((r) => (resolve = r))
      return { response: { count: get(baseCountAtom) } }
    },
  }))

  const Counter = () => {
    const [{ data }] = useAtom(countAtom)

    return (
      <>
        <div>count: {data.response.count}</div>
      </>
    )
  }

  const Controls = () => {
    const [, increment] = useAtom(incrementAtom)
    return <button onClick={increment}>increment</button>
  }

  const { getByText, findByText } = render(
    <StrictMode>
      <Suspense fallback={'loading'}>
        <Counter />
      </Suspense>
      <Controls />
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('count: 0')

  fireEvent.click(getByText('increment'))
  await findByText('loading')
  resolve()
  await findByText('count: 1')
})

it('query expected QueryCache test', async () => {
  const queryClient = new QueryClient()
  let resolve = () => {}
  const countAtom = atomWithSuspenseQuery(
    () => ({
      queryKey: ['count6'],
      queryFn: async () => {
        await new Promise<void>((r) => (resolve = r))
        return { response: { count: 0 } }
      },
    }),
    () => queryClient
  )
  const Counter = () => {
    const [{ data }] = useAtom(countAtom)

    return (
      <>
        <div>count: {data.response.count}</div>
      </>
    )
  }

  const { findByText } = render(
    <StrictMode>
      <Suspense fallback="loading">
        <Counter />
      </Suspense>
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('count: 0')
  expect(queryClient.getQueryCache().getAll().length).toBe(1)
})

describe('error handling', () => {
  it('can catch error in error boundary', async () => {
    let resolve = () => {}
    const countAtom = atomWithSuspenseQuery(() => ({
      queryKey: ['catch'],
      retry: false,
      queryFn: async (): Promise<{ response: { count: number } }> => {
        await new Promise<void>((r) => (resolve = r))
        throw new Error('fetch error')
      },
    }))
    const Counter = () => {
      const [{ data }] = useAtom(countAtom)
      return <div>count: {data.response.count}</div>
    }
    const { findByText } = render(
      <StrictMode>
        <ErrorBoundary fallback={<>errored</>}>
          <Suspense fallback={'loading'}>
            <Counter />
          </Suspense>
        </ErrorBoundary>
      </StrictMode>
    )
    await findByText('loading')
    resolve()
    await findByText('errored')
  })
  it('can recover from error', async () => {
    let count = -1
    let willThrowError = false
    let resolve = () => {}
    const countAtom = atomWithSuspenseQuery(() => ({
      queryKey: ['error test', 'count2'],
      retry: false,
      queryFn: async () => {
        willThrowError = !willThrowError
        ++count
        await new Promise<void>((r) => (resolve = r))
        if (willThrowError) {
          throw new Error('fetch error')
        }
        return { response: { count } }
      },
      throwOnError: true,
    }))
    const Counter = () => {
      const [{ data, refetch, error, isFetching }] = useAtom(countAtom)

      if (error && !isFetching) {
        throw error
      }

      return (
        <>
          <div>count: {data?.response.count}</div>
          <button onClick={() => refetch()}>refetch</button>
        </>
      )
    }

    const FallbackComponent: React.FC<{ resetErrorBoundary: () => void }> = ({
      resetErrorBoundary,
    }) => {
      const refresh = useSetAtom(countAtom)
      return (
        <>
          <h1>errored</h1>
          <button
            onClick={() => {
              refresh()
              resetErrorBoundary()
            }}>
            retry
          </button>
        </>
      )
    }
    const App = () => {
      return (
        <Provider>
          <ErrorBoundary FallbackComponent={FallbackComponent}>
            <Suspense fallback="loading">
              <Counter />
            </Suspense>
          </ErrorBoundary>
        </Provider>
      )
    }
    const { findByText, getByText } = render(<App />)
    await findByText('loading')
    resolve()
    await findByText('errored')
    fireEvent.click(getByText('retry'))
    await findByText('loading')
    resolve()
    await findByText('count: 1')
    fireEvent.click(getByText('refetch'))
    resolve()
    await findByText('errored')
    fireEvent.click(getByText('retry'))
    resolve()
    await findByText('count: 3')
  })
})

it('renews the result when the query changes and a non stale cache is available', async () => {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { staleTime: 5 * 60 * 1000 } },
  })
  queryClient.setQueryData([2], 2)

  const currentCountAtom = atom(1)

  const countAtom = atomWithSuspenseQuery(
    (get) => {
      const currentCount = get(currentCountAtom)
      return {
        queryKey: [currentCount],
        queryFn: () => currentCount,
      }
    },
    () => queryClient
  )

  const Counter = () => {
    const setCurrentCount = useSetAtom(currentCountAtom)
    const [{ data }] = useAtom(countAtom)

    return (
      <>
        <button onClick={() => setCurrentCount(2)}>Set count to 2</button>
        <div>count: {data}</div>
      </>
    )
  }

  const { findByText } = render(
    <StrictMode>
      <Suspense fallback={'loading'}>
        <Counter />
      </Suspense>
    </StrictMode>
  )
  await findByText('loading')
  await findByText('count: 1')
  fireEvent.click(await findByText('Set count to 2'))
  await expect(() => findByText('loading')).rejects.toThrow()
  await findByText('count: 2')
})

it('on reset, throws suspense', async () => {
  const queryClient = new QueryClient()
  let count = 0
  let resolve = () => {}
  const countAtom = atomWithSuspenseQuery(
    () => ({
      queryKey: ['test1', count],
      queryFn: async () => {
        await new Promise<void>((r) => (resolve = r))
        count++
        return { response: { count } }
      },
    }),
    () => queryClient
  )
  const Counter = () => {
    const [{ data }] = useAtom(countAtom)
    return (
      <>
        <div>count: {data.response.count}</div>
        <button
          onClick={() => queryClient.resetQueries({ queryKey: ['test1'] })}>
          reset
        </button>
      </>
    )
  }

  const { findByText, getByText } = render(
    <StrictMode>
      <Suspense fallback="loading">
        <Counter />
      </Suspense>
    </StrictMode>
  )

  await findByText('loading')
  resolve()
  await findByText('count: 1')
  fireEvent.click(getByText('reset'))
  await findByText('loading')
  resolve()
  await findByText('count: 2')
})

// https://github.com/jotaijs/jotai-tanstack-query/pull/40
it(`ensure that setQueryData for an inactive query updates its atom state`, async () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        refetchOnMount: false,
      },
    },
  })

  const extraKey = 'uniqueKey'
  const pageAtom = atom(1)

  const queryFn = vi.fn(() => {
    return Promise.resolve('John Doe')
  })

  const userAtom = atomWithSuspenseQuery(
    () => {
      return {
        queryKey: [extraKey],
        queryFn: async () => {
          const name = await queryFn()
          return { response: { name } }
        },
      }
    },
    () => queryClient
  )

  const User = () => {
    const [
      {
        data: {
          response: { name },
        },
      },
    ] = useAtom(userAtom)

    return <>Name: {name}</>
  }

  const Controls = () => {
    const [, setPage] = useAtom(pageAtom)
    return (
      <>
        <button onClick={() => setPage(1)}>Set page 1</button>
        <button onClick={() => setPage(2)}>Set page 2</button>
      </>
    )
  }

  const App = () => {
    const [page] = useAtom(pageAtom)
    return (
      <>
        <Suspense fallback="loading">{page === 1 && <User />}</Suspense>
        <Controls />
      </>
    )
  }

  const { findByText } = render(
    <StrictMode>
      <App />
    </StrictMode>
  )

  await findByText('loading')
  await findByText('Name: John Doe')
  fireEvent.click(await findByText('Set page 2'))
  queryClient.setQueryData([extraKey], { response: { name: 'Alex Smith' } })
  fireEvent.click(await findByText('Set page 1'))
  await expect(() => findByText('loading')).rejects.toThrow()
  await findByText('Name: Alex Smith')
})


================================================
FILE: eslint.config.js
================================================
import js from '@eslint/js'
import typescript from '@typescript-eslint/eslint-plugin'
import typescriptParser from '@typescript-eslint/parser'
import importPlugin from 'eslint-plugin-import'
import prettier from 'eslint-plugin-prettier'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'

export default [
  js.configs.recommended,
  {
    files: ['**/*.{js,jsx,ts,tsx}'],
    languageOptions: {
      parser: typescriptParser,
      parserOptions: {
        ecmaVersion: 2018,
        sourceType: 'module',
        ecmaFeatures: {
          jsx: true,
        },
      },
      globals: {
        // Browser globals
        window: 'readonly',
        document: 'readonly',
        console: 'readonly',
        fetch: 'readonly',
        URL: 'readonly',
        URLSearchParams: 'readonly',
        // Node.js globals
        process: 'readonly',
        Buffer: 'readonly',
        __dirname: 'readonly',
        __filename: 'readonly',
        global: 'readonly',
        // ES6 globals
        Promise: 'readonly',
        Symbol: 'readonly',
        Map: 'readonly',
        Set: 'readonly',
        WeakMap: 'readonly',
        WeakSet: 'readonly',
      },
    },
    plugins: {
      '@typescript-eslint': typescript,
      react,
      'react-hooks': reactHooks,
      import: importPlugin,
      prettier,
    },
    rules: {
      // ESLint recommended rules are included via js.configs.recommended

      // Custom rules
      eqeqeq: 'error',
      'no-var': 'error',
      'prefer-const': 'error',
      curly: ['warn', 'multi-line', 'consistent'],
      'no-console': 'off',

      // Import rules
      'import/no-unresolved': ['error', { commonjs: true, amd: true }],
      'import/export': 'error',
      'import/no-duplicates': ['error'],
      'import/order': [
        'error',
        {
          alphabetize: { order: 'asc', caseInsensitive: true },
          groups: [
            'builtin',
            'external',
            'internal',
            'parent',
            'sibling',
            'index',
            'object',
          ],
          'newlines-between': 'never',
          pathGroups: [
            {
              pattern: 'react',
              group: 'builtin',
              position: 'before',
            },
          ],
          pathGroupsExcludedImportTypes: ['builtin'],
        },
      ],

      // TypeScript rules
      '@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/no-redeclare': 'off',
      'no-unused-vars': 'off', // Use TypeScript version instead
      'no-redeclare': 'off', // Use TypeScript version instead

      // React rules
      'react/jsx-uses-react': 'off',
      'react/react-in-jsx-scope': 'off',

      // React hooks rules
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',

      // Sort imports
      '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'],
        },
        alias: {
          extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
          map: [['jotai-tanstack-query', './src/index.ts']],
        },
      },
    },
  },
  {
    files: ['src/**/*.{ts,tsx}'],
    languageOptions: {
      parserOptions: {
        project: './tsconfig.json',
      },
    },
  },
  {
    files: ['tests/**/*.tsx', '__tests__/**/*'],
    languageOptions: {
      globals: {
        describe: 'readonly',
        it: 'readonly',
        test: 'readonly',
        expect: 'readonly',
        beforeEach: 'readonly',
        afterEach: 'readonly',
        beforeAll: 'readonly',
        afterAll: 'readonly',
        vi: 'readonly',
      },
    },
  },
  {
    files: ['./*.js'],
    rules: {
      '@typescript-eslint/no-var-requires': 'off',
    },
  },
  {
    files: ['examples/**/*.{ts,tsx}'],
    rules: {
      'import/no-unresolved': 'off',
    },
  },
  {
    ignores: ['dist/**', 'src/vendor/**', 'node_modules/**', '**/.next/**'],
  },
]


================================================
FILE: examples/01_query/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>jotai-tanstack-query example</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>


================================================
FILE: examples/01_query/package.json
================================================
{
  "name": "jotai-tanstack-query-example-query",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@tanstack/query-core": "latest",
    "jotai": "latest",
    "jotai-tanstack-query": "*",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "@types/react": "latest",
    "@types/react-dom": "latest",
    "@vitejs/plugin-react": "latest",
    "typescript": "latest",
    "vite": "latest"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}


================================================
FILE: examples/01_query/src/App.tsx
================================================
import { Getter } from 'jotai'
import { useAtom } from 'jotai/react'
import { atom } from 'jotai/vanilla'
import { atomWithQuery } from 'jotai-tanstack-query'

const idAtom = atom(1)

const userAtom = atomWithQuery((get: Getter) => ({
  queryKey: ['users', get(idAtom).toString()],
  queryFn: async ({ queryKey: [, id] }) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    return res.json()
  },
}))

const UserData = () => {
  const [{ data, isPending, isError }] = useAtom(userAtom)

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>

  return <div>{JSON.stringify(data)}</div>
}

const Controls = () => {
  const [id, setId] = useAtom(idAtom)
  return (
    <div>
      ID: {id}{' '}
      <button type="button" onClick={() => setId((c) => c - 1)}>
        Prev
      </button>{' '}
      <button type="button" onClick={() => setId((c) => c + 1)}>
        Next
      </button>
    </div>
  )
}

const App = () => (
  <>
    <Controls />
    <UserData />
  </>
)

export default App


================================================
FILE: examples/01_query/src/index.tsx
================================================
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

const ele = document.getElementById('app')
if (ele) {
  createRoot(ele).render(React.createElement(App))
}


================================================
FILE: examples/01_query/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: examples/01_query/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/01_query/vite.config.ts
================================================
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})


================================================
FILE: examples/02_suspense/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>jotai-tanstack-query example</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>


================================================
FILE: examples/02_suspense/package.json
================================================
{
  "name": "jotai-tanstack-query-example-suspense",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@tanstack/query-core": "latest",
    "jotai": "latest",
    "jotai-tanstack-query": "*",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "@types/react": "latest",
    "@types/react-dom": "latest",
    "@vitejs/plugin-react": "latest",
    "typescript": "latest",
    "vite": "latest"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}


================================================
FILE: examples/02_suspense/src/App.tsx
================================================
import { Suspense } from 'react'
import { useAtom } from 'jotai/react'
import { atom } from 'jotai/vanilla'
import { atomWithSuspenseQuery } from 'jotai-tanstack-query'

const idAtom = atom(1)

const userAtom = atomWithSuspenseQuery<object>((get) => ({
  queryKey: ['users', get(idAtom)],
  queryFn: async ({ queryKey: [, id] }) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    return res.json()
  },
}))

const UserData = () => {
  const [{ data }] = useAtom(userAtom)

  return <div>{JSON.stringify(data)}</div>
}

const Controls = () => {
  const [id, setId] = useAtom(idAtom)
  return (
    <div>
      ID: {id}{' '}
      <button type="button" onClick={() => setId((c) => c - 1)}>
        Prev
      </button>{' '}
      <button type="button" onClick={() => setId((c) => c + 1)}>
        Next
      </button>
    </div>
  )
}

const App = () => (
  <>
    <Controls />
    <Suspense fallback="loading">
      <UserData />
    </Suspense>
  </>
)

export default App


================================================
FILE: examples/02_suspense/src/index.tsx
================================================
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

const ele = document.getElementById('app')
if (ele) {
  createRoot(ele).render(React.createElement(App))
}


================================================
FILE: examples/02_suspense/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: examples/02_suspense/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/02_suspense/vite.config.ts
================================================
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})


================================================
FILE: examples/03_infinite/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>jotai-tanstack-query example</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>


================================================
FILE: examples/03_infinite/package.json
================================================
{
  "name": "jotai-tanstack-query-example-infinite",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@tanstack/query-core": "latest",
    "jotai": "latest",
    "jotai-tanstack-query": "*",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "@types/react": "latest",
    "@types/react-dom": "latest",
    "@vitejs/plugin-react": "latest",
    "typescript": "latest",
    "vite": "latest"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}


================================================
FILE: examples/03_infinite/src/App.tsx
================================================
import { useAtom } from 'jotai'
import { atomWithInfiniteQuery } from 'jotai-tanstack-query'

const postsAtom = atomWithInfiniteQuery(() => ({
  queryKey: ['posts'],
  queryFn: async ({ pageParam }) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`
    )
    return res.json()
  },
  getNextPageParam: (...args) => {
    return args[2] + 1
  },
  initialPageParam: 0,
}))

const Posts = () => {
  const [{ data, fetchNextPage, isPending, isError, isFetching }] =
    useAtom(postsAtom)

  if (isFetching || isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>

  return (
    <>
      {data?.pages.map((page, index) => (
        <div key={index}>
          {page.map((post: any) => (
            <div key={post.id}>{post.title}</div>
          ))}
        </div>
      ))}
      <button onClick={() => fetchNextPage()}>Next</button>
    </>
  )
}

const App = () => (
  <>
    <Posts />
  </>
)

export default App


================================================
FILE: examples/03_infinite/src/index.tsx
================================================
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

const ele = document.getElementById('app')
if (ele) {
  createRoot(ele).render(React.createElement(App))
}


================================================
FILE: examples/03_infinite/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: examples/03_infinite/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/03_infinite/vite.config.ts
================================================
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})


================================================
FILE: examples/04_infinite_suspense/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>jotai-tanstack-query example</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>


================================================
FILE: examples/04_infinite_suspense/package.json
================================================
{
  "name": "jotai-tanstack-query-example-infinite-suspense",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@tanstack/query-core": "latest",
    "jotai": "latest",
    "jotai-tanstack-query": "*",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "@types/react": "latest",
    "@types/react-dom": "latest",
    "@vitejs/plugin-react": "latest",
    "typescript": "latest",
    "vite": "latest"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}


================================================
FILE: examples/04_infinite_suspense/src/App.tsx
================================================
import { Suspense } from 'react'
import { useAtom } from 'jotai/react'
import { atomWithSuspenseInfiniteQuery } from 'jotai-tanstack-query'

const postsAtom = atomWithSuspenseInfiniteQuery(() => ({
  initialPageParam: 1,
  queryKey: ['posts'],
  queryFn: async ({ pageParam = 1 }) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/posts/${pageParam}`
    )
    const data: { id: number; title: string } = await res.json()
    return data
  },
  getNextPageParam: (lastPage) => lastPage.id + 1,
}))

const Posts = () => {
  const [{ data, fetchNextPage }] = useAtom(postsAtom)
  return (
    <div>
      <button onClick={() => fetchNextPage()}>Next</button>
      <ul>
        {data.pages.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  )
}

const App = () => (
  <>
    <Suspense fallback="Loading...">
      <Posts />
    </Suspense>
  </>
)

export default App


================================================
FILE: examples/04_infinite_suspense/src/index.tsx
================================================
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

const ele = document.getElementById('app')
if (ele) {
  createRoot(ele).render(React.createElement(App))
}


================================================
FILE: examples/04_infinite_suspense/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: examples/04_infinite_suspense/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/04_infinite_suspense/vite.config.ts
================================================
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})


================================================
FILE: examples/05_mutation/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>jotai-tanstack-query example</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>


================================================
FILE: examples/05_mutation/package.json
================================================
{
  "name": "jotai-tanstack-query-example-mutation",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@tanstack/query-core": "latest",
    "jotai": "latest",
    "jotai-tanstack-query": "*",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "@types/react": "latest",
    "@types/react-dom": "latest",
    "@vitejs/plugin-react": "latest",
    "typescript": "latest",
    "vite": "latest"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}


================================================
FILE: examples/05_mutation/src/App.tsx
================================================
import React from 'react'
import { useAtom } from 'jotai/react'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'

interface Post {
  id: number
  title: string
  body: string
  userId: number
}

interface NewPost {
  title: string
}

interface OptimisticContext {
  previousPosts: Post[] | undefined
}

// Query to fetch posts list
const postsQueryAtom = atomWithQuery(() => ({
  queryKey: ['posts'],
  queryFn: async () => {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
    return res.json() as Promise<Post[]>
  },
}))

// Mutation with optimistic updates
const postAtom = atomWithMutation<Post, NewPost, Error, OptimisticContext>(
  (get) => {
    const queryClient = get(queryClientAtom)
    return {
      mutationKey: ['addPost'],
      mutationFn: async ({ title }: NewPost) => {
        // Randomly fail for testing error handling (30% failure rate)
        if (Math.random() < 0.3) {
          throw new Error('Randomly simulated API error')
        }

        const res = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
          method: 'POST',
          body: JSON.stringify({
            title,
            body: 'body',
            userId: 1,
          }),
          headers: {
            'Content-type': 'application/json; charset=UTF-8',
          },
        })
        const data = await res.json()
        return data as Post
      },
      // When mutate is called:
      onMutate: async (newPost: NewPost) => {
        // Cancel any outgoing refetches
        // (so they don't overwrite our optimistic update)
        await queryClient.cancelQueries({ queryKey: ['posts'] })

        // Snapshot the previous value
        const previousPosts = queryClient.getQueryData<Post[]>(['posts'])

        // Optimistically update to the new value
        queryClient.setQueryData<Post[]>(['posts'], (old) => {
          const optimisticPost: Post = {
            id: Date.now(), // Temporary ID
            title: newPost.title,
            body: 'body',
            userId: 1,
          }
          return old ? [...old, optimisticPost] : [optimisticPost]
        })

        // Return a result with the snapshotted value
        return { previousPosts }
      },
      // If the mutation fails, use the result returned from onMutate to roll back
      onError: (
        _err: Error,
        _newPost: NewPost,
        onMutateResult: OptimisticContext | undefined
      ) => {
        console.debug('onError', onMutateResult)
        if (onMutateResult?.previousPosts) {
          queryClient.setQueryData(['posts'], onMutateResult.previousPosts)
        }
      },
      // Always refetch after error or success:
      onSettled: (
        _data: Post | undefined,
        _error: Error | null,
        _variables: NewPost,
        _onMutateResult: OptimisticContext | undefined
      ) => {
        queryClient.invalidateQueries({ queryKey: ['posts'] })
      },
    }
  }
)

const PostsList = () => {
  const [{ data: posts, isPending }] = useAtom(postsQueryAtom)

  if (isPending) return <div>Loading posts...</div>

  return (
    <div>
      <h3>Posts:</h3>
      <ul>
        {posts?.map((post: Post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

const AddPost = () => {
  const [{ mutate, isPending, status }] = useAtom(postAtom)
  const [title, setTitle] = React.useState('')

  return (
    <div>
      <div>
        <input
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="Enter post title"
        />
        <button
          onClick={() => {
            if (title) {
              mutate({ title })
              setTitle('')
            }
          }}
          disabled={isPending}
        >
          {isPending ? 'Adding...' : 'Add Post'}
        </button>
      </div>
      <div>
        <strong>Status:</strong> {status}
      </div>
    </div>
  )
}

const App = () => (
  <>
    <h2>atomWithMutation with optimistic updates</h2>
    <PostsList />
    <AddPost />
  </>
)

export default App


================================================
FILE: examples/05_mutation/src/index.tsx
================================================
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

const ele = document.getElementById('app')
if (ele) {
  createRoot(ele).render(React.createElement(App))
}


================================================
FILE: examples/05_mutation/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: examples/05_mutation/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/05_mutation/vite.config.ts
================================================
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})


================================================
FILE: examples/06_refetch/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>jotai-tanstack-query example</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>


================================================
FILE: examples/06_refetch/package.json
================================================
{
  "name": "jotai-tanstack-query-example-refetch",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@tanstack/query-core": "latest",
    "jotai": "latest",
    "jotai-tanstack-query": "*",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "@types/react": "latest",
    "@types/react-dom": "latest",
    "@vitejs/plugin-react": "latest",
    "typescript": "latest",
    "vite": "latest"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}


================================================
FILE: examples/06_refetch/src/App.tsx
================================================
import { useAtom } from 'jotai/react'
import { atom } from 'jotai/vanilla'
import { atomWithQuery } from 'jotai-tanstack-query'
import { ErrorBoundary } from 'react-error-boundary'
import type { FallbackProps } from 'react-error-boundary'

const idAtom = atom(1)

const userAtom = atomWithQuery((get) => ({
  queryKey: ['users', get(idAtom)],
  queryFn: async ({ queryKey: [, id] }) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    return res.json()
  },
}))

const UserData = () => {
  const [{ data, refetch, isPending }] = useAtom(userAtom)
  if (isPending) return <div>Loading...</div>
  return (
    <div>
      <ul>
        <li>ID: {data.id}</li>
        <li>Username: {data.username}</li>
        <li>Email: {data.email}</li>
      </ul>
      <button onClick={() => refetch()}>refetch</button>
    </div>
  )
}

const Controls = () => {
  const [id, setId] = useAtom(idAtom)
  return (
    <div>
      ID: {id}{' '}
      <button type="button" onClick={() => setId((c) => c - 1)}>
        Prev
      </button>{' '}
      <button type="button" onClick={() => setId((c) => c + 1)}>
        Next
      </button>
    </div>
  )
}

const Fallback = ({ error, resetErrorBoundary }: FallbackProps) => {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button type="button" onClick={resetErrorBoundary}>
        Try again
      </button>
    </div>
  )
}

const App = () => (
  <ErrorBoundary FallbackComponent={Fallback}>
    <Controls />
    <UserData />
  </ErrorBoundary>
)

export default App


================================================
FILE: examples/06_refetch/src/index.tsx
================================================
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

const ele = document.getElementById('app')
if (ele) {
  createRoot(ele).render(React.createElement(App))
}


================================================
FILE: examples/06_refetch/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: examples/06_refetch/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/06_refetch/vite.config.ts
================================================
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})


================================================
FILE: examples/07_queries/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>jotai-tanstack-query example</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>


================================================
FILE: examples/07_queries/package.json
================================================
{
  "name": "jotai-tanstack-query-example-queries",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@tanstack/query-core": "latest",
    "jotai": "latest",
    "jotai-tanstack-query": "*",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "@types/react": "latest",
    "@types/react-dom": "latest",
    "@vitejs/plugin-react": "latest",
    "typescript": "latest",
    "vite": "latest"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}


================================================
FILE: examples/07_queries/src/App.tsx
================================================
import { Atom, atom } from 'jotai'
import { useAtom } from 'jotai/react'
import { type AtomWithQueryResult, atomWithQueries } from 'jotai-tanstack-query'

const userIdsAtom = atom([1, 2, 3])

interface User {
  id: number
  name: string
  email: string
}

const userQueryAtomsAtom = atom((get) => {
  const userIds = get(userIdsAtom)
  return atomWithQueries<User>({
    queries: userIds.map((id) => () => ({
      queryKey: ['user', id],
      queryFn: async ({ queryKey: [, userId] }) => {
        const res = await fetch(
          `https://jsonplaceholder.typicode.com/users/${userId}`
        )
        return res.json()
      },
    })),
  })
})

const UsersData = () => {
  const [userQueryAtoms] = useAtom(userQueryAtomsAtom)
  return (
    <div>
      <h3>Users: </h3>
      <div>
        {userQueryAtoms.map((queryAtom, index) => (
          <Data key={index} queryAtom={queryAtom} />
        ))}
      </div>
    </div>
  )
}

const combinedUsersDataAtom = atom((get) => {
  const userIds = get(userIdsAtom)
  return atomWithQueries<{
    data: User[]
    isPending: boolean
    isError: boolean
  }>({
    queries: userIds.map((id) => () => ({
      queryKey: ['user', id],
      queryFn: async ({ queryKey: [, userId] }) => {
        const res = await fetch(
          `https://jsonplaceholder.typicode.com/users/${userId}`
        )
        return res.json()
      },
    })),
    combine: (results) => {
      return {
        data: results.map((result) => result.data as User),
        isPending: results.some((result) => result.isPending),
        isError: results.some((result) => result.isError),
      }
    },
  })
})

const CombinedUsersData = () => {
  const [combinedUsersDataAtomValue] = useAtom(combinedUsersDataAtom)
  const [{ data, isPending, isError }] = useAtom(combinedUsersDataAtomValue)

  return (
    <div>
      <h3>Users: (combinedQueries)</h3>
      {isPending && <div>Loading...</div>}
      {isError && <div>Error</div>}
      {!isPending && !isError && (
        <div>
          {data.map((user) => (
            <UserDisplay key={user.id} user={user} />
          ))}
        </div>
      )}
    </div>
  )
}

const Data = ({
  queryAtom,
}: {
  queryAtom: Atom<AtomWithQueryResult<User>>
}) => {
  const [{ data, isPending, isError }] = useAtom(queryAtom)

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>
  if (!data) return null

  return <UserDisplay user={data} />
}

const UserDisplay = ({ user }: { user: User }) => {
  return (
    <div>
      <div>ID: {user.id}</div>
      <strong>{user.name}</strong> - {user.email}
    </div>
  )
}

const Controls = () => {
  const [userIds, setUserIds] = useAtom(userIdsAtom)

  return (
    <div>
      <div>User IDs: {userIds.join(', ')} </div>
      <button
        onClick={() => {
          const n = Math.floor(Math.random() * 8)
          setUserIds([n + 1, n + 2, n + 3])
        }}>
        Random
      </button>
    </div>
  )
}

const App = () => {
  return (
    <div>
      <Controls />
      <UsersData />
      <hr />
      <CombinedUsersData />
    </div>
  )
}

export default App


================================================
FILE: examples/07_queries/src/index.tsx
================================================
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

const ele = document.getElementById('app')
if (ele) {
  createRoot(ele).render(React.createElement(App))
}


================================================
FILE: examples/07_queries/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: examples/07_queries/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/07_queries/vite.config.ts
================================================
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})


================================================
FILE: examples/08_query_client_atom_provider/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>jotai-tanstack-query example</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>


================================================
FILE: examples/08_query_client_atom_provider/package.json
================================================
{
  "name": "jotai-tanstack-query-example-query-client-atom-provider",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@tanstack/query-core": "latest",
    "@tanstack/react-query": "latest",
    "jotai": "latest",
    "jotai-tanstack-query": "*",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "@types/react": "latest",
    "@types/react-dom": "latest",
    "@vitejs/plugin-react": "latest",
    "typescript": "latest",
    "vite": "latest"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}


================================================
FILE: examples/08_query_client_atom_provider/src/App.tsx
================================================
import { QueryClient, useQuery } from '@tanstack/react-query'
import { useAtom } from 'jotai/react'
import { atom } from 'jotai/vanilla'
import { atomWithQuery } from 'jotai-tanstack-query'
import { QueryClientAtomProvider } from 'jotai-tanstack-query/react'

const queryClient = new QueryClient()

const idAtom = atom(1)

const userAtom = atomWithQuery(
  (get) => ({
    queryKey: ['users', get(idAtom)],
  }),
  () => queryClient
)

const UserDataRawFetch = () => {
  const [id] = useAtom(idAtom)
  const { data, isPending, isError } = useQuery({
    queryKey: ['users', id],
    queryFn: async ({ queryKey: [, id] }) => {
      const res = await fetch(
        `https://jsonplaceholder.typicode.com/users/${id}`
      )
      return res.json()
    },
  })

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>

  return (
    <>
      <h2>Tanstack Query</h2>
      <div>{JSON.stringify(data)}</div>
    </>
  )
}

const UserData = () => {
  const [{ data, isPending, isError }] = useAtom(userAtom)

  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>

  return (
    <>
      <h2>Jotai-Tanstack-Query</h2>
      <div>{JSON.stringify(data)}</div>
    </>
  )
}

const Controls = () => {
  const [id, setId] = useAtom(idAtom)
  return (
    <>
      <div>
        ID: {id}{' '}
        <button type="button" onClick={() => setId((c) => c - 1)}>
          Prev
        </button>{' '}
        <button type="button" onClick={() => setId((c) => c + 1)}>
          Next
        </button>
      </div>
      <div>
        <button
          type="button"
          onClick={() =>
            queryClient.setQueryData(['users', id], (old: any) => {
              return {
                ...old,
                name: old.name + '🔄',
              }
            })
          }>
          Update User Data
        </button>
      </div>
    </>
  )
}

const App = () => (
  <>
    <QueryClientAtomProvider client={queryClient}>
      <Controls />
      <UserDataRawFetch />
      <UserData />
    </QueryClientAtomProvider>
  </>
)

export default App


================================================
FILE: examples/08_query_client_atom_provider/src/index.tsx
================================================
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

const ele = document.getElementById('app')
if (ele) {
  createRoot(ele).render(React.createElement(App))
}


================================================
FILE: examples/08_query_client_atom_provider/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: examples/08_query_client_atom_provider/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/08_query_client_atom_provider/vite.config.ts
================================================
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})


================================================
FILE: examples/09_error_boundary/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>jotai-tanstack-query example</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>


================================================
FILE: examples/09_error_boundary/package.json
================================================
{
  "name": "jotai-tanstack-query-example-error-boundary",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@tanstack/query-core": "latest",
    "jotai": "latest",
    "jotai-tanstack-query": "*",
    "react": "latest",
    "react-dom": "latest",
    "react-error-boundary": "latest"
  },
  "devDependencies": {
    "@types/react": "latest",
    "@types/react-dom": "latest",
    "@vitejs/plugin-react": "latest",
    "typescript": "latest",
    "vite": "latest"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}


================================================
FILE: examples/09_error_boundary/src/App.tsx
================================================
import { Suspense } from 'react'
import { atom, useAtom, useSetAtom } from 'jotai'
import { atomWithSuspenseQuery } from 'jotai-tanstack-query'
import { ErrorBoundary, type FallbackProps } from 'react-error-boundary'

const idAtom = atom(1)
const userAtom = atomWithSuspenseQuery<User>((get) => ({
  queryKey: ['user', get(idAtom)],
  queryFn: async ({ queryKey: [, id] }) => {
    const randomNumber = Math.floor(Math.random() * 10)
    if (randomNumber % 3 === 0) {
      await fetch(`https://jsonplaceholder.typicode.com/users/error`)
      return await Promise.reject('fetch failed')
    }

    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    return res.json()
  },
  retry: false,
}))

const UserData = () => {
  const [{ data }] = useAtom(userAtom)

  return (
    <>
      <UserDisplay user={data} />
    </>
  )
}

interface User {
  id: number
  name: string
  email: string
}

const UserDisplay = ({ user }: { user: User }) => {
  return (
    <div>
      <div>ID: {user.id}</div>
      <strong>{user.name}</strong> - {user.email}
    </div>
  )
}

const Controls = () => {
  const [id, setId] = useAtom(idAtom)
  return (
    <>
      <div>
        ID: {id}{' '}
        <button type="button" onClick={() => setId((c) => c - 1)}>
          Prev
        </button>{' '}
        <button type="button" onClick={() => setId((c) => c + 1)}>
          Next
        </button>
      </div>
    </>
  )
}

const Fallback = ({ error, resetErrorBoundary }: FallbackProps) => {
  const reset = useSetAtom(userAtom)
  const retry = () => {
    reset()
    resetErrorBoundary()
  }
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={retry}>Try again</button>
    </div>
  )
}

const App = () => {
  return (
    <ErrorBoundary FallbackComponent={Fallback}>
      <Suspense fallback="Loading...">
        <Controls />
        <UserData />
      </Suspense>
    </ErrorBoundary>
  )
}

export default App


================================================
FILE: examples/09_error_boundary/src/index.tsx
================================================
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

const ele = document.getElementById('app')
if (ele) {
  createRoot(ele).render(React.createElement(App))
}


================================================
FILE: examples/09_error_boundary/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: examples/09_error_boundary/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: examples/09_error_boundary/vite.config.ts
================================================
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})


================================================
FILE: examples/11_nextjs_app_router/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts


================================================
FILE: examples/11_nextjs_app_router/README.md
================================================
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.


================================================
FILE: examples/11_nextjs_app_router/eslint.config.mjs
================================================
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

const eslintConfig = defineConfig([
  ...nextVitals,
  ...nextTs,
  // Override default ignores of eslint-config-next.
  globalIgnores([
    // Default ignores of eslint-config-next:
    ".next/**",
    "out/**",
    "build/**",
    "next-env.d.ts",
  ]),
]);

export default eslintConfig;


================================================
FILE: examples/11_nextjs_app_router/next.config.ts
================================================
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
};

export default nextConfig;


================================================
FILE: examples/11_nextjs_app_router/package.json
================================================
{
  "name": "jotai-tanstack-query-example-nextjs-app-router",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@tanstack/react-query": "^5.90.10",
    "jotai": "^2.15.1",
    "jotai-tanstack-query": "*",
    "next": "~15.4.7",
    "react": "19.2.0",
    "react-dom": "19.2.0"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "~15.4.7",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}


================================================
FILE: examples/11_nextjs_app_router/postcss.config.mjs
================================================
const config = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

export default config;


================================================
FILE: examples/11_nextjs_app_router/src/app/api.ts
================================================
export async function getPost(postId: string) {
  console.debug('getPost called with postId:', postId)
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${postId}`
  )
  return res.json()
}


================================================
FILE: examples/11_nextjs_app_router/src/app/layout.tsx
================================================
import type { ReactNode } from 'react'
import Providers from './providers'

export default function RootLayout({
  children,
}: Readonly<{
  children: ReactNode
}>) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}


================================================
FILE: examples/11_nextjs_app_router/src/app/page.tsx
================================================
import { redirect } from 'next/navigation'

export default function Home() {
  redirect('/posts/1')
}


================================================
FILE: examples/11_nextjs_app_router/src/app/posts/[postId]/_components/post.tsx
================================================
'use client'

import { useEffect } from 'react'
import { useAtomValue, useSetAtom } from 'jotai'
import Link from 'next/link'
import { postIdAtom, postQueryAtom } from '../../../stores'

export const Post = ({ postId }: { postId: string }) => {
  const setPostIdValue = useSetAtom(postIdAtom)
  useEffect(() => {
    setPostIdValue(postId)
  }, [postId, setPostIdValue])

  const { data, isPending, isError, refetch } = useAtomValue(postQueryAtom)
  if (isPending) return <div>Loading...</div>
  if (isError) return <div>Error</div>
  return (
    <div>
      <Link href="/posts">Back to posts</Link>
      <div>ID: {data?.id}</div>
      <h1>Title: {data?.title}</h1>
      <div>Body: {data?.body}</div>

      <div style={{ marginTop: '1rem' }}>
        <button onClick={() => refetch()}>
          Refetch post - only client-side
        </button>
      </div>

      <div style={{ marginTop: '1rem' }}>
        <Link href={`/posts/${Number(postId) + 1}`}>
          Next post - only server-side
        </Link>
      </div>
    </div>
  )
}


================================================
FILE: examples/11_nextjs_app_router/src/app/posts/[postId]/page.tsx
================================================
// app/posts/[postId]/page.tsx
import {
  HydrationBoundary,
  QueryClient,
  dehydrate,
} from '@tanstack/react-query'
import { Post } from './_components/post'
import { getPost } from '@/app/api'

export default async function PostPage({
  params,
}: {
  params: Promise<{ postId: string }>
}) {
  const { postId } = await params
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts', postId],
    queryFn: ({ queryKey: [, postId] }) => getPost(postId as string),
  })

  return (
    // Neat! Serialization is now as easy as passing props.
    // HydrationBoundary is a Client Component, so hydration will happen there.
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Post postId={postId} />
    </HydrationBoundary>
  )
}


================================================
FILE: examples/11_nextjs_app_router/src/app/posts/page.tsx
================================================
import Link from 'next/link'

export default function PostsPage() {
  return (
    <div>
      Posts
      <div>
        <Link href="/posts/1">Post 1</Link>
        <br />
        <Link href="/posts/2">Post 2</Link>
        <br />
        <Link href="/posts/3">Post 3</Link>
        <br />
        <Link href="/posts/4">Post 4</Link>
        <br />
        <Link href="/posts/5">Post 5</Link>
        <br />
        <Link href="/posts/6">Post 6</Link>
        <br />
        <Link href="/posts/7">Post 7</Link>
        <br />
        <Link href="/posts/8">Post 8</Link>
        <br />
        <Link href="/posts/9">Post 9</Link>
        <br />
        <Link href="/posts/10">Post 10</Link>
      </div>
    </div>
  )
}


================================================
FILE: examples/11_nextjs_app_router/src/app/providers.tsx
================================================
// In Next.js, this file would be called: app/providers.tsx
'use client'

// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
import type { ReactNode } from 'react'
import {
  QueryClient,
  QueryClientProvider,
  QueryClientProviderProps,
  isServer,
} from '@tanstack/react-query'
import { Provider } from 'jotai/react'
import { useHydrateAtoms } from 'jotai/react/utils'
import { queryClientAtom } from 'jotai-tanstack-query'

const HydrateAtoms = ({ client, children }: QueryClientProviderProps) => {
  useHydrateAtoms(new Map([[queryClientAtom, client]]))
  return children
}
function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // With SSR, we usually want to set some default staleTime
        // above 0 to avoid refetching immediately on the client
        staleTime: 60 * 1000,
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined = undefined

function getQueryClient() {
  if (isServer) {
    // Server: always make a new query client
    return makeQueryClient()
  } else {
    // Browser: make a new query client if we don't already have one
    // This is very important, so we don't re-make a new client if React
    // suspends during the initial render. This may not be needed if we
    // have a suspense boundary BELOW the creation of the query client
    if (!browserQueryClient) browserQueryClient = makeQueryClient()
    return browserQueryClient
  }
}

export default function Providers({ children }: { children: ReactNode }) {
  // NOTE: Avoid useState when initializing the query client if you don't
  //       have a suspense boundary between this and the code that may
  //       suspend because React will throw away the client on the initial
  //       render if it suspends and there is no boundary
  const queryClient = getQueryClient()

  return (
    <QueryClientProvider client={queryClient}>
      <Provider>
        <HydrateAtoms client={queryClient}>{children}</HydrateAtoms>
      </Provider>
    </QueryClientProvider>
  )
}


================================================
FILE: examples/11_nextjs_app_router/src/app/stores.ts
================================================
import { atom } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
import { getPost } from './api'

export const postIdAtom = atom<string>('1')
export const postQueryAtom = atomWithQuery((get) => ({
  queryKey: ['posts', get(postIdAtom)],
  queryFn: ({ queryKey: [, postId] }) => getPost(postId as string),
  staleTime: Infinity,
  refetchOnMount: false,
  refetchOnWindowFocus: false,
}))


================================================
FILE: examples/11_nextjs_app_router/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".next/dev/types/**/*.ts",
    "**/*.mts"
  ],
  "exclude": ["node_modules", ".next"]
}


================================================
FILE: package.json
================================================
{
  "name": "jotai-tanstack-query",
  "description": "👻🌺",
  "version": "0.11.0",
  "type": "module",
  "author": "Daishi Kato",
  "contributors": [
    "Thaddeus Jiang",
    "Mohammad Bagher Abiat",
    "Kali Charan Reddy Jonna"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/jotaijs/jotai-tanstack-query.git"
  },
  "source": "./src/index.ts",
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    "./package.json": "./package.json",
    ".": {
      "types": "./dist/index.d.ts",
      "module": "./dist/index.mjs",
      "import": "./dist/index.mjs",
      "default": "./dist/index.js"
    },
    "./react": {
      "types": "./dist/react.d.ts",
      "module": "./dist/react.js",
      "import": "./dist/react.mjs",
      "default": "./dist/react.js"
    }
  },
  "sideEffects": false,
  "files": [
    "src",
    "dist"
  ],
  "scripts": {
    "compile": "bunchee --sourcemap",
    "test": "run-s eslint tsc-test vitest",
    "eslint": "eslint --ext .js,.ts,.tsx .",
    "vitest": "vitest run",
    "test:dev": "vitest",
    "tsc-test": "tsc --project . --noEmit",
    "examples:01_query": "pnpm --filter ./examples/01_query run dev",
    "examples:02_suspense": "pnpm --filter ./examples/02_suspense run dev",
    "examples:03_infinite": "pnpm --filter ./examples/03_infinite run dev",
    "examples:04_infinite_suspense": "pnpm --filter ./examples/04_infinite_suspense run dev",
    "examples:05_mutation": "pnpm --filter ./examples/05_mutation run dev",
    "examples:06_refetch": "pnpm --filter ./examples/06_refetch run dev",
    "examples:07_queries": "pnpm --filter ./examples/07_queries run dev",
    "examples:08_query_client_atom_provider": "pnpm --filter ./examples/08_query_client_atom_provider run dev",
    "examples:09_error_boundary": "pnpm --filter ./examples/09_error_boundary run dev",
    "examples:11_nextjs_app_router": "pnpm --filter ./examples/11_nextjs_app_router run dev"
  },
  "keywords": [
    "jotai",
    "react",
    "tanstack/query",
    "state management",
    "data fetching"
  ],
  "license": "MIT",
  "devDependencies": {
    "@eslint/js": "^9.32.0",
    "@tanstack/query-core": "^5.83.0",
    "@tanstack/react-query": "^5.83.0",
    "@testing-library/dom": "^10.4.0",
    "@testing-library/react": "^16.3.0",
    "@types/jsdom": "^21.1.7",
    "@types/node": "^22.13.14",
    "@types/react": "^19.1.8",
    "@types/react-dom": "^19.1.6",
    "@typescript-eslint/eslint-plugin": "^8.38.0",
    "@typescript-eslint/parser": "^8.38.0",
    "@vitest/ui": "^3.2.4",
    "bunchee": "^6.5.4",
    "eslint": "^9.32.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-import-resolver-alias": "^1.1.2",
    "eslint-plugin-import": "^2.31.0",
    "eslint-plugin-prettier": "^5.2.1",
    "eslint-plugin-react": "^7.37.5",
    "eslint-plugin-react-hooks": "^5.1.0",
    "jotai": "^2.4.2",
    "jsdom": "^25.0.1",
    "npm-run-all": "^4.1.5",
    "prettier": "^3.0.3",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-error-boundary": "^4.0.11",
    "typescript": "^5.8.3",
    "vitest": "^3.2.4"
  },
  "peerDependencies": {
    "@tanstack/query-core": "*",
    "@tanstack/react-query": "*",
    "jotai": ">=2.0.0",
    "react": "^18.0.0 || ^19.0.0"
  },
  "peerDependenciesMeta": {
    "@tanstack/react-query": {
      "optional": true
    },
    "react": {
      "optional": true
    }
  },
  "packageManager": "pnpm@10.13.1"
}


================================================
FILE: pnpm-workspace.yaml
================================================
linkWorkspacePackages: true
preferWorkspacePackages: true
preferFrozenLockfile: true

packages:
  - examples/*

onlyBuiltDependencies:
  - '@swc/core'
  - esbuild


================================================
FILE: src/_queryClientAtom.ts
================================================
import { QueryClient } from '@tanstack/query-core'
import { atom } from 'jotai/vanilla'

export const queryClientAtom = atom(new QueryClient())

if (process.env.NODE_ENV !== 'production') {
  queryClientAtom.debugPrivate = true
}


================================================
FILE: src/atomWithInfiniteQuery.ts
================================================
import {
  type DefaultError,
  type InfiniteData,
  InfiniteQueryObserver,
  QueryClient,
  type QueryKey,
  QueryObserver,
} from '@tanstack/query-core'
import { Getter, WritableAtom } from 'jotai'
import { queryClientAtom } from './_queryClientAtom'
import { baseAtomWithQuery } from './baseAtomWithQuery'
import {
  AtomWithInfiniteQueryOptions,
  AtomWithInfiniteQueryResult,
  DefinedAtomWithInfiniteQueryResult,
  DefinedInitialDataInfiniteOptions,
  UndefinedInitialDataInfiniteOptions,
} from './types'

export function atomWithInfiniteQuery<
  TQueryFnData,
  TError = DefaultError,
  TData = InfiniteData<TQueryFnData>,
  TQueryKey extends QueryKey = QueryKey,
  TPageParam = unknown,
>(
  getOptions: (
    get: Getter
  ) => UndefinedInitialDataInfiniteOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryKey,
    TPageParam
  >,
  getQueryClient?: (get: Getter) => QueryClient
): WritableAtom<AtomWithInfiniteQueryResult<TData, TError>, [], void>
export function atomWithInfiniteQuery<
  TQueryFnData,
  TError = DefaultError,
  TData = InfiniteData<TQueryFnData>,
  TQueryKey extends QueryKey = QueryKey,
  TPageParam = unknown,
>(
  getOptions: (
    get: Getter
  ) => DefinedInitialDataInfiniteOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryKey,
    TPageParam
  >,
  getQueryClient?: (get: Getter) => QueryClient
): WritableAtom<DefinedAtomWithInfiniteQueryResult<TData, TError>, [], void>
export function atomWithInfiniteQuery<
  TQueryFnData,
  TError = DefaultError,
  TData = InfiniteData<TQueryFnData>,
  TQueryKey extends QueryKey = QueryKey,
  TPageParam = unknown,
>(
  getOptions: (
    get: Getter
  ) => AtomWithInfiniteQueryOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryKey,
    TPageParam
  >,
  getQueryClient?: (get: Getter) => QueryClient
): WritableAtom<AtomWithInfiniteQueryResult<TData, TError>, [], void>
export function atomWithInfiniteQuery(
  getOptions: (get: Getter) => AtomWithInfiniteQueryOptions,
  getQueryClient: (get: Getter) => QueryClient = (get) => get(queryClientAtom)
) {
  return baseAtomWithQuery(
    getOptions,
    InfiniteQueryObserver as typeof QueryObserver,
    getQueryClient
  )
}


================================================
FILE: src/atomWithMutation.ts
================================================
import {
  MutationObserver,
  type MutationOptions,
  QueryClient,
  notifyManager,
} from '@tanstack/query-core'
import { Atom, Getter, atom } from 'jotai'
import { queryClientAtom } from './_queryClientAtom'
import { AtomWithMutationResult, MutateFunction } from './types'
import { shouldThrowError } from './utils'

export function atomWithMutation<
  TData = unknown,
  TVariables = void,
  TError = unknown,
  TContext = unknown,
>(
  getOptions: (
    get: Getter
  ) => MutationOptions<TData, TError, TVariables, TContext>,
  getQueryClient: (get: Getter) => QueryClient = (get) => get(queryClientAtom)
): Atom<AtomWithMutationResult<TData, TError, TVariables, TContext>> {
  const IN_RENDER = Symbol()

  const optionsAtom = atom((get) => {
    const client = getQueryClient(get)
    const options = getOptions(get)
    return client.defaultMutationOptions(options)
  })
  if (process.env.NODE_ENV !== 'production') {
    optionsAtom.debugPrivate = true
  }

  const observerCacheAtom = atom(
    () =>
      new WeakMap<
        QueryClient,
        MutationObserver<TData, TError, TVariables, TContext>
      >()
  )
  if (process.env.NODE_ENV !== 'production') {
    observerCacheAtom.debugPrivate = true
  }

  const observerAtom = atom((get) => {
    const options = get(optionsAtom)
    const client = getQueryClient(get)
    const observerCache = get(observerCacheAtom)

    const observer = observerCache.get(client)

    if (observer) {
      ;(observer as any)[IN_RENDER] = true
      observer.setOptions(options)
      delete (observer as any)[IN_RENDER]

      return observer
    }

    const newObserver = new MutationObserver(client, options)
    observerCache.set(client, newObserver)

    return newObserver
  })
  if (process.env.NODE_ENV !== 'production') {
    observerAtom.debugPrivate = true
  }

  const dataAtom = atom((get) => {
    const observer = get(observerAtom)

    const currentResult = observer.getCurrentResult()
    const resultAtom = atom(currentResult)

    resultAtom.onMount = (set) => {
      observer.subscribe(notifyManager.batchCalls(set))
      return () => {
        observer.reset()
      }
    }
    if (process.env.NODE_ENV !== 'production') {
      resultAtom.debugPrivate = true
    }

    return resultAtom
  })

  const mutateAtom = atom((get) => {
    const observer = get(observerAtom)
    const mutate: MutateFunction<TData, TError, TVariables, TContext> = (
      variables,
      options
    ) => {
      observer.mutate(variables, options).catch(noop)
    }

    return mutate
  })
  if (process.env.NODE_ENV !== 'production') {
    mutateAtom.debugPrivate = true
  }

  return atom((get) => {
    const observer = get(observerAtom)
    const resultAtom = get(dataAtom)

    const result = get(resultAtom)
    const mutate = get(mutateAtom)

    if (
      result.isError &&
      shouldThrowError(observer.options.throwOnError, [result.error])
    ) {
      throw result.error
    }

    return { ...result, mutate, mutateAsync: result.mutate }
  })
}

function noop() {}


================================================
FILE: src/atomWithMutationState.ts
================================================
import {
  type DefaultError,
  Mutation,
  MutationCache,
  type MutationFilters,
  type MutationState,
  QueryClient,
} from '@tanstack/query-core'
import { Getter, atom } from 'jotai'
import { queryClientAtom } from './_queryClientAtom'

type MutationStateOptions<TResult = MutationState> = {
  filters?: MutationFilters
  select?: (
    mutation: Mutation<unknown, DefaultError, unknown, unknown>
  ) => TResult
}

function getResult<TResult = MutationState>(
  mutationCache: MutationCache,
  options: MutationStateOptions<TResult>
): Array<TResult> {
  return mutationCache
    .findAll(options.filters)
    .map(
      (mutation): TResult =>
        (options.select
          ? options.select(
              mutation as Mutation<unknown, DefaultError, unknown, unknown>
            )
          : mutation.state) as TResult
    )
}

export const atomWithMutationState = <TResult = MutationState>(
  getOptions: (get: Getter) => MutationStateOptions<TResult>,
  getQueryClient: (get: Getter) => QueryClient = (get) => get(queryClientAtom)
) => {
  const resultsAtom = atom<TResult[]>([])
  if (process.env.NODE_ENV !== 'production') {
    resultsAtom.debugPrivate = true
  }

  const observableAtom = atom((get) => {
    const queryClient = getQueryClient(get)

    const mutationCache = queryClient.getMutationCache()
    resultsAtom.onMount = (set) => {
      const unsubscribe = mutationCache.subscribe(() => {
        set(getResult(getQueryClient(get).getMutationCache(), getOptions(get)))
      })
      return unsubscribe
    }
  })
  if (process.env.NODE_ENV !== 'production') {
    observableAtom.debugPrivate = true
  }

  return atom((get) => {
    get(observableAtom)
    return get(resultsAtom)
  })
}


================================================
FILE: src/atomWithQueries.ts
================================================
import {
  type DefaultError,
  QueryClient,
  QueryObserver,
} from '@tanstack/query-core'
import { Getter, WritableAtom, atom } from 'jotai'
import { queryClientAtom } from './_queryClientAtom'
import { baseAtomWithQuery } from './baseAtomWithQuery'
import { AtomWithQueryOptions, AtomWithQueryResult } from './types'

export function atomWithQueries<TCombinedResult>(
  {
    queries,
    combine,
  }: {
    queries: Array<(get: Getter) => AtomWithQueryOptions>
    combine: (results: AtomWithQueryResult[]) => TCombinedResult
  },
  getQueryClient?: (get: Getter) => QueryClient
): WritableAtom<TCombinedResult, [], void>
export function atomWithQueries<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
>(
  {
    queries,
  }: {
    queries: Array<(get: Getter) => AtomWithQueryOptions>
  },
  getQueryClient?: (get: Getter) => QueryClient
): Array<WritableAtom<AtomWithQueryResult<TData, TError>, [], void>>

export function atomWithQueries(
  {
    queries,
    combine,
  }: {
    queries: Array<(get: Getter) => AtomWithQueryOptions>
    combine?: (results: AtomWithQueryResult[]) => any
  },
  getQueryClient: (get: Getter) => QueryClient = (get) => get(queryClientAtom)
): any {
  if (!combine) {
    return queries.map((getOptions) =>
      baseAtomWithQuery(getOptions, QueryObserver, getQueryClient)
    )
  }

  const queryAtoms = queries.map((getOptions) =>
    baseAtomWithQuery(getOptions, QueryObserver, getQueryClient)
  )

  return atom((get) => {
    const results = queryAtoms.map((queryAtom) => {
      const result = get(queryAtom)
      return result as AtomWithQueryResult
    })
    return combine(results)
  })
}


================================================
FILE: src/atomWithQuery.ts
================================================
import {
  type DefaultError,
  QueryClient,
  type QueryKey,
  QueryObserver,
} from '@tanstack/query-core'
import { Getter, WritableAtom } from 'jotai'
import { queryClientAtom } from './_queryClientAtom'
import { baseAtomWithQuery } from './baseAtomWithQuery'
import {
  AtomWithQueryOptions,
  AtomWithQueryResult,
  DefinedAtomWithQueryResult,
  DefinedInitialDataOptions,
  UndefinedInitialDataOptions,
} from './types'

export function atomWithQuery<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  getOptions: (
    get: Getter
  ) => UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
  getQueryClient?: (get: Getter) => QueryClient
): WritableAtom<AtomWithQueryResult<TData, TError>, [], void>
export function atomWithQuery<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  getOptions: (
    get: Getter
  ) => DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
  getQueryClient?: (get: Getter) => QueryClient
): WritableAtom<DefinedAtomWithQueryResult<TData, TError>, [], void>
export function atomWithQuery<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  getOptions: (
    get: Getter
  ) => AtomWithQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
  getQueryClient?: (get: Getter) => QueryClient
): WritableAtom<AtomWithQueryResult<TData, TError>, [], void>
export function atomWithQuery(
  getOptions: (get: Getter) => AtomWithQueryOptions,
  getQueryClient: (get: Getter) => QueryClient = (get) => get(queryClientAtom)
) {
  return baseAtomWithQuery(getOptions, QueryObserver, getQueryClient)
}


================================================
FILE: src/atomWithSuspenseInfiniteQuery.ts
================================================
import {
  type DefaultError,
  type InfiniteData,
  InfiniteQueryObserver,
  QueryClient,
  type QueryKey,
  QueryObserver,
} from '@tanstack/query-core'
import { Getter, WritableAtom, atom } from 'jotai'
import { queryClientAtom } from './_queryClientAtom'
import { baseAtomWithQuery } from './baseAtomWithQuery'
import {
  AtomWithSuspenseInfiniteQueryOptions,
  AtomWithSuspenseInfiniteQueryResult,
} from './types'
import { defaultThrowOnError } from './utils'

export function atomWithSuspenseInfiniteQuery<
  TQueryFnData,
  TError = DefaultError,
  TData = InfiniteData<TQueryFnData>,
  TQueryKey extends QueryKey = QueryKey,
  TPageParam = unknown,
>(
  getOptions: (
    get: Getter
  ) => AtomWithSuspenseInfiniteQueryOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryKey,
    TPageParam
  >,
  getQueryClient: (get: Getter) => QueryClient = (get) => get(queryClientAtom)
): WritableAtom<AtomWithSuspenseInfiniteQueryResult<TData, TError>, [], void> {
  const suspenseOptionsAtom = atom((get) => {
    const options = getOptions(get)
    return {
      ...options,
      enabled: true,
      suspense: true,
      throwOnError: defaultThrowOnError,
    }
  })

  return baseAtomWithQuery(
    (get: Getter) => get(suspenseOptionsAtom),
    InfiniteQueryObserver as typeof QueryObserver,
    getQueryClient
  ) as unknown as WritableAtom<
    AtomWithSuspenseInfiniteQueryResult<TData, TError>,
    [],
    void
  >
}


================================================
FILE: src/atomWithSuspenseQuery.ts
================================================
import {
  type DefaultError,
  QueryClient,
  type QueryKey,
  QueryObserver,
} from '@tanstack/query-core'
import { Getter, WritableAtom, atom } from 'jotai'
import { queryClientAtom } from './_queryClientAtom'
import { baseAtomWithQuery } from './baseAtomWithQuery'
import {
  AtomWithSuspenseQueryOptions,
  AtomWithSuspenseQueryResult,
} from './types'
import { defaultThrowOnError } from './utils'

export function atomWithSuspenseQuery<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  getOptions: (
    get: Getter
  ) => AtomWithSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
  getQueryClient: (get: Getter) => QueryClient = (get) => get(queryClientAtom)
): WritableAtom<AtomWithSuspenseQueryResult<TData, TError>, [], void> {
  const suspenseOptions = atom((get) => {
    const options = getOptions(get)
    return {
      ...options,
      suspense: true,
      enabled: true,
      throwOnError: defaultThrowOnError,
    }
  })

  return baseAtomWithQuery(
    (get: Getter) => get(suspenseOptions),
    QueryObserver,
    getQueryClient
  ) as WritableAtom<AtomWithSuspenseQueryResult<TData, TError>, [], void>
}


================================================
FILE: src/baseAtomWithQuery.ts
================================================
import {
  QueryClient,
  type QueryKey,
  QueryObserver,
  type QueryObserverResult,
  notifyManager,
} from '@tanstack/query-core'
import { Getter, WritableAtom, atom } from 'jotai'
import { queryClientAtom } from './_queryClientAtom'
import { BaseAtomWithQueryOptions } from './types'
import { ensureStaleTime, getHasError, shouldSuspend } from './utils'

export function baseAtomWithQuery<
  TQueryFnData,
  TError,
  TData,
  TQueryData,
  TQueryKey extends QueryKey,
>(
  getOptions: (
    get: Getter
  ) => BaseAtomWithQueryOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryData,
    TQueryKey
  >,
  Observer: typeof QueryObserver,
  getQueryClient: (get: Getter) => QueryClient = (get) => get(queryClientAtom)
): WritableAtom<
  | QueryObserverResult<TData, TError>
  | Promise<QueryObserverResult<TData, TError>>,
  [],
  void
> {
  const refreshAtom = atom(0)
  const clientAtom = atom(getQueryClient)
  if (process.env.NODE_ENV !== 'production') {
    clientAtom.debugPrivate = true
  }

  const observerCacheAtom = atom(
    () =>
      new WeakMap<
        QueryClient,
        QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>
      >()
  )
  if (process.env.NODE_ENV !== 'production') {
    observerCacheAtom.debugPrivate = true
  }

  const defaultedOptionsAtom = atom((get) => {
    const client = get(clientAtom)
    const options = getOptions(get)
    const defaultedOptions = client.defaultQueryOptions(options)

    const cache = get(observerCacheAtom)
    const cachedObserver = cache.get(client)

    defaultedOptions._optimisticResults = 'optimistic'

    if (cachedObserver) {
      cachedObserver.setOptions(defaultedOptions)
    }

    return ensureStaleTime(defaultedOptions)
  })
  if (process.env.NODE_ENV !== 'production') {
    defaultedOptionsAtom.debugPrivate = true
  }

  const observerAtom = atom((get) => {
    const client = get(clientAtom)
    const defaultedOptions = get(defaultedOptionsAtom)

    const observerCache = get(observerCacheAtom)

    const cachedObserver = observerCache.get(client)

    if (cachedObserver) return cachedObserver

    const newObserver = new Observer(client, defaultedOptions)
    observerCache.set(client, newObserver)

    return newObserver
  })
  if (process.env.NODE_ENV !== 'production') {
    observerAtom.debugPrivate = true
  }

  const dataAtom = atom((get) => {
    const observer = get(observerAtom)
    const defaultedOptions = get(defaultedOptionsAtom)
    const result = observer.getOptimisticResult(defaultedOptions)

    const resultAtom = atom(result)
    if (process.env.NODE_ENV !== 'production') {
      resultAtom.debugPrivate = true
    }

    resultAtom.onMount = (set) => {
      const unsubscribe = observer.subscribe(notifyManager.batchCalls(set))
      return () => {
        if (observer.getCurrentResult().isError) {
          observer.getCurrentQuery().reset()
        }
        unsubscribe()
      }
    }

    return resultAtom
  })
  if (process.env.NODE_ENV !== 'production') {
    dataAtom.debugPrivate = true
  }

  return atom(
    (get) => {
      get(refreshAtom)
      const observer = get(observerAtom)
      const defaultedOptions = get(defaultedOptionsAtom)

      const result = get(get(dataAtom))

      if (shouldSuspend(defaultedOptions, result, false)) {
        return observer.fetchOptimistic(defaultedOptions)
      }

      if (
        getHasError({
          result,
          query: observer.getCurrentQuery(),
          throwOnError: defaultedOptions.throwOnError,
        })
      ) {
        throw result.error
      }

      return result
    },
    (_get, set) => {
      set(refreshAtom, (c) => c + 1)
    }
  )
}


================================================
FILE: src/index.ts
================================================
export { queryClientAtom } from './_queryClientAtom'
export { atomWithQuery } from './atomWithQuery'
export { atomWithQueries } from './atomWithQueries'
export { atomWithSuspenseQuery } from './atomWithSuspenseQuery'
export { atomWithInfiniteQuery } from './atomWithInfiniteQuery'
export { atomWithMutation } from './atomWithMutation'
export { atomWithSuspenseInfiniteQuery } from './atomWithSuspenseInfiniteQuery'
export { atomWithMutationState } from './atomWithMutationState'
export * from './types'


================================================
FILE: src/react.ts
================================================
import { createElement } from 'react'
import {
  QueryClientProvider,
  type QueryClientProviderProps,
} from '@tanstack/react-query'
import { Provider } from 'jotai'
import { useHydrateAtoms } from 'jotai/utils'
import { queryClientAtom } from './_queryClientAtom'

const HydrateAtoms = ({ client, children }: QueryClientProviderProps) => {
  useHydrateAtoms([[queryClientAtom, client]])
  return children
}

export function QueryClientAtomProvider({
  client,
  children,
}: QueryClientProviderProps) {
  return createElement(
    QueryClientProvider,
    { client },
    createElement(
      Provider,
      null,
      createElement(HydrateAtoms, { client }, children)
    )
  )
}


================================================
FILE: src/types.ts
================================================
import {
  type DefaultError,
  type DefinedInfiniteQueryObserverResult,
  type DefinedQueryObserverResult,
  type InfiniteData,
  type InfiniteQueryObserverOptions,
  type InfiniteQueryObserverResult,
  type MutationObserverOptions,
  type MutationObserverResult,
  type QueryKey,
  type MutateFunction as QueryMutateFunction,
  type QueryObserverOptions,
  type QueryObserverResult,
  type WithRequired,
} from '@tanstack/query-core'

type Override<A, B> = { [K in keyof A]: K extends keyof B ? B[K] : A[K] }

export type MutateFunction<
  TData = unknown,
  TError = DefaultError,
  TVariables = void,
  TContext = unknown,
> = (
  ...args: Parameters<QueryMutateFunction<TData, TError, TVariables, TContext>>
) => void

export type MutateAsyncFunction<
  TData = unknown,
  TError = DefaultError,
  TVariables = void,
  TContext = unknown,
> = QueryMutateFunction<TData, TError, TVariables, TContext>

export type AtomWithMutationResult<TData, TError, TVariables, TContext> =
  Override<
    MutationObserverResult<TData, TError, TVariables, TContext>,
    { mutate: MutateFunction<TData, TError, TVariables, TContext> }
  > & {
    mutateAsync: MutateAsyncFunction<TData, TError, TVariables, TContext>
  }

export type MutationOptions<
  TData = unknown,
  TError = DefaultError,
  TVariables = void,
  TContext = unknown,
> = Omit<
  MutationObserverOptions<TData, TError, TVariables, TContext>,
  '_defaulted' | 'variables'
>

export type BaseAtomWithQueryOptions<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> = WithRequired<
  QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
  'queryKey'
>

export type AtomWithQueryOptions<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> = Omit<
  WithRequired<
    BaseAtomWithQueryOptions<
      TQueryFnData,
      TError,
      TData,
      TQueryFnData,
      TQueryKey
    >,
    'queryKey'
  >,
  'suspense'
>

export type AtomWithSuspenseQueryOptions<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> = Omit<
  AtomWithQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
  'enabled' | 'throwOnError' | 'placeholderData'
>

export type AtomWithInfiniteQueryOptions<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
  TPageParam = unknown,
> = WithRequired<
  Omit<
    InfiniteQueryObserverOptions<
      TQueryFnData,
      TError,
      TData,
      TQueryKey,
      TPageParam
    >,
    'suspense'
  >,
  'queryKey'
>

export type AtomWithSuspenseInfiniteQueryOptions<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
  TPageParam = unknown,
> = Omit<
  AtomWithInfiniteQueryOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryKey,
    TPageParam
  >,
  'enabled' | 'throwOnError' | 'placeholderData'
>

export type AtomWithQueryResult<
  TData = unknown,
  TError = DefaultError,
> = QueryObserverResult<TData, TError>

export type DefinedAtomWithQueryResult<
  TData = unknown,
  TError = DefaultError,
> = DefinedQueryObserverResult<TData, TError>

export type AtomWithSuspenseQueryResult<
  TData = unknown,
  TError = DefaultError,
> =
  | Omit<DefinedQueryObserverResult<TData, TError>, 'isPlaceholderData'>
  | Promise<
      Omit<DefinedQueryObserverResult<TData, TError>, 'isPlaceholderData'>
    >

export type AtomWithInfiniteQueryResult<
  TData = unknown,
  TError = DefaultError,
> = InfiniteQueryObserverResult<TData, TError>

export type DefinedAtomWithInfiniteQueryResult<
  TData = unknown,
  TError = DefaultError,
> = DefinedInfiniteQueryObserverResult<TData, TError>

export type AtomWithSuspenseInfiniteQueryResult<
  TData = unknown,
  TError = DefaultError,
> = Promise<
  Omit<DefinedInfiniteQueryObserverResult<TData, TError>, 'isPlaceholderData'>
>

export type UndefinedInitialDataOptions<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> = AtomWithQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
  initialData?: undefined
}

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

export type DefinedInitialDataOptions<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> = AtomWithQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
  initialData:
    | NonUndefinedGuard<TQueryFnData>
    | (() => NonUndefinedGuard<TQueryFnData>)
}

export type UndefinedInitialDataInfiniteOptions<
  TQueryFnData,
  TError = DefaultError,
  TData = InfiniteData<TQueryFnData>,
  TQueryKey extends QueryKey = QueryKey,
  TPageParam = unknown,
> = AtomWithInfiniteQueryOptions<
  TQueryFnData,
  TError,
  TData,
  TQueryKey,
  TPageParam
> & {
  initialData?: undefined
}

export type DefinedInitialDataInfiniteOptions<
  TQueryFnData,
  TError = DefaultError,
  TData = InfiniteData<TQueryFnData>,
  TQueryKey extends QueryKey = QueryKey,
  TPageParam = unknown,
> = AtomWithInfiniteQueryOptions<
  TQueryFnData,
  TError,
  TData,
  TQueryKey,
  TPageParam
> & {
  initialData:
    | NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
    | (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
}


================================================
FILE: src/utils.ts
================================================
import type {
  DefaultError,
  DefaultedQueryObserverOptions,
  Query,
  QueryKey,
  QueryObserverResult,
  ThrowOnError,
} from '@tanstack/query-core'

export const shouldSuspend = (
  defaultedOptions:
    | DefaultedQueryObserverOptions<any, any, any, any, any>
    | undefined,
  result: QueryObserverResult<any, any>,
  isRestoring: boolean
) => defaultedOptions?.suspense && willFetch(result, isRestoring)

export const willFetch = (
  result: QueryObserverResult<any, any>,
  isRestoring: boolean
) => result.isPending && !isRestoring

export const getHasError = <
  TData,
  TError,
  TQueryFnData,
  TQueryData,
  TQueryKey extends QueryKey,
>({
  result,
  throwOnError,
  query,
}: {
  result: QueryObserverResult<TData, TError>
  throwOnError:
    | ThrowOnError<TQueryFnData, TError, TQueryData, TQueryKey>
    | undefined
  query: Query<TQueryFnData, TError, TQueryData, TQueryKey>
}) => {
  return (
    result.isError &&
    !result.isFetching &&
    shouldThrowError(throwOnError, [result.error, query])
  )
}

export function shouldThrowError<T extends (...args: any[]) => boolean>(
  throwOnError: boolean | T | undefined,
  params: Parameters<T>
): boolean {
  // Allow useErrorBoundary function to override throwing behavior on a per-error basis
  if (typeof throwOnError === 'function') {
    return throwOnError(...params)
  }

  return !!throwOnError
}

export const defaultThrowOnError = <
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  _error: TError,
  query: Query<TQueryFnData, TError, TData, TQueryKey>
) => typeof query.state.data === 'undefined'

export const ensureStaleTime = (
  defaultedOptions: DefaultedQueryObserverOptions<any, any, any, any, any>
) => {
  if (defaultedOptions.suspense) {
    if (typeof defaultedOptions.staleTime !== 'number') {
      return {
        ...defaultedOptions,
        staleTime: 1000,
      }
    }
  }

  return defaultedOptions
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "strict": true,
    "target": "esnext",
    "downlevelIteration": true,
    "esModuleInterop": true,
    "module": "es2015",
    "moduleResolution": "bundler",
    "allowJs": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "jsx": "react",
    "baseUrl": ".",
    "skipLibCheck": true,
    "paths": {
      "jotai-tanstack-query": ["./src"]
    },
    "types": ["vitest/globals", "jsdom"]
  },
  "include": ["__tests__", "src"],
  "exclude": ["node_modules", "dist"]
}


================================================
FILE: vitest.config.ts
================================================
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: [],
    projects: ['vitest.config.ts'],
    include: ['__tests__/**/*.spec.tsx'],
    testTimeout: 10000, // Increase timeout for tests with fake timers
  },
  resolve: {
    alias: {
      'jotai-tanstack-query': new URL('./src/index.ts', import.meta.url)
        .pathname,
    },
  },
})
Download .txt
gitextract_m_gm3mha/

├── .github/
│   └── workflows/
│       ├── cd.yml
│       ├── ci.yml
│       └── pr.yml
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__/
│   ├── 01_basic.spec.tsx
│   ├── atomWithInfiniteQuery.spec.tsx
│   ├── atomWithMutation.spec.tsx
│   ├── atomWithMutationState.spec.tsx
│   ├── atomWithQuery.spec.tsx
│   ├── atomWithSuspenseInfiniteQuery.spec.tsx
│   └── atomWithSuspenseQuery.spec.tsx
├── eslint.config.js
├── examples/
│   ├── 01_query/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 02_suspense/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 03_infinite/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 04_infinite_suspense/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 05_mutation/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 06_refetch/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 07_queries/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 08_query_client_atom_provider/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   ├── 09_error_boundary/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── App.tsx
│   │   │   └── index.tsx
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   └── 11_nextjs_app_router/
│       ├── .gitignore
│       ├── README.md
│       ├── eslint.config.mjs
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── src/
│       │   └── app/
│       │       ├── api.ts
│       │       ├── layout.tsx
│       │       ├── page.tsx
│       │       ├── posts/
│       │       │   ├── [postId]/
│       │       │   │   ├── _components/
│       │       │   │   │   └── post.tsx
│       │       │   │   └── page.tsx
│       │       │   └── page.tsx
│       │       ├── providers.tsx
│       │       └── stores.ts
│       └── tsconfig.json
├── package.json
├── pnpm-workspace.yaml
├── src/
│   ├── _queryClientAtom.ts
│   ├── atomWithInfiniteQuery.ts
│   ├── atomWithMutation.ts
│   ├── atomWithMutationState.ts
│   ├── atomWithQueries.ts
│   ├── atomWithQuery.ts
│   ├── atomWithSuspenseInfiniteQuery.ts
│   ├── atomWithSuspenseQuery.ts
│   ├── baseAtomWithQuery.ts
│   ├── index.ts
│   ├── react.ts
│   ├── types.ts
│   └── utils.ts
├── tsconfig.json
└── vitest.config.ts
Download .txt
SYMBOL INDEX (57 symbols across 24 files)

FILE: __tests__/atomWithInfiniteQuery.spec.tsx
  type DataResponse (line 21) | type DataResponse = { response: { count: number } }
  type DataResponse (line 128) | type DataResponse = {
  type DataResponse (line 196) | type DataResponse = {
  class ErrorBoundary (line 282) | class ErrorBoundary extends Component<
    method constructor (line 286) | constructor(props: { message?: string; children: ReactNode }) {
    method getDerivedStateFromError (line 290) | static getDerivedStateFromError() {
    method render (line 293) | render() {

FILE: __tests__/atomWithMutation.spec.tsx
  function App (line 18) | function App() {
  function TestView (line 29) | function TestView() {

FILE: __tests__/atomWithMutationState.spec.tsx
  function App (line 46) | function App() {

FILE: __tests__/atomWithSuspenseInfiniteQuery.spec.tsx
  type DataResponse (line 8) | type DataResponse = {

FILE: examples/05_mutation/src/App.tsx
  type Post (line 5) | interface Post {
  type NewPost (line 12) | interface NewPost {
  type OptimisticContext (line 16) | interface OptimisticContext {

FILE: examples/07_queries/src/App.tsx
  type User (line 7) | interface User {

FILE: examples/09_error_boundary/src/App.tsx
  type User (line 32) | interface User {

FILE: examples/11_nextjs_app_router/src/app/api.ts
  function getPost (line 1) | async function getPost(postId: string) {

FILE: examples/11_nextjs_app_router/src/app/layout.tsx
  function RootLayout (line 4) | function RootLayout({

FILE: examples/11_nextjs_app_router/src/app/page.tsx
  function Home (line 3) | function Home() {

FILE: examples/11_nextjs_app_router/src/app/posts/[postId]/page.tsx
  function PostPage (line 10) | async function PostPage({

FILE: examples/11_nextjs_app_router/src/app/posts/page.tsx
  function PostsPage (line 3) | function PostsPage() {

FILE: examples/11_nextjs_app_router/src/app/providers.tsx
  function makeQueryClient (line 20) | function makeQueryClient() {
  function getQueryClient (line 34) | function getQueryClient() {
  function Providers (line 48) | function Providers({ children }: { children: ReactNode }) {

FILE: src/atomWithInfiniteQuery.ts
  function atomWithInfiniteQuery (line 74) | function atomWithInfiniteQuery(

FILE: src/atomWithMutation.ts
  function atomWithMutation (line 12) | function atomWithMutation<
  function noop (line 121) | function noop() {}

FILE: src/atomWithMutationState.ts
  type MutationStateOptions (line 12) | type MutationStateOptions<TResult = MutationState> = {
  function getResult (line 19) | function getResult<TResult = MutationState>(

FILE: src/atomWithQueries.ts
  function atomWithQueries (line 34) | function atomWithQueries(

FILE: src/atomWithQuery.ts
  function atomWithQuery (line 51) | function atomWithQuery(

FILE: src/atomWithSuspenseInfiniteQuery.ts
  function atomWithSuspenseInfiniteQuery (line 18) | function atomWithSuspenseInfiniteQuery<

FILE: src/atomWithSuspenseQuery.ts
  function atomWithSuspenseQuery (line 16) | function atomWithSuspenseQuery<

FILE: src/baseAtomWithQuery.ts
  function baseAtomWithQuery (line 13) | function baseAtomWithQuery<

FILE: src/react.ts
  function QueryClientAtomProvider (line 15) | function QueryClientAtomProvider({

FILE: src/types.ts
  type Override (line 17) | type Override<A, B> = { [K in keyof A]: K extends keyof B ? B[K] : A[K] }
  type MutateFunction (line 19) | type MutateFunction<
  type MutateAsyncFunction (line 28) | type MutateAsyncFunction<
  type AtomWithMutationResult (line 35) | type AtomWithMutationResult<TData, TError, TVariables, TContext> =
  type MutationOptions (line 43) | type MutationOptions<
  type BaseAtomWithQueryOptions (line 53) | type BaseAtomWithQueryOptions<
  type AtomWithQueryOptions (line 64) | type AtomWithQueryOptions<
  type AtomWithSuspenseQueryOptions (line 83) | type AtomWithSuspenseQueryOptions<
  type AtomWithInfiniteQueryOptions (line 93) | type AtomWithInfiniteQueryOptions<
  type AtomWithSuspenseInfiniteQueryOptions (line 113) | type AtomWithSuspenseInfiniteQueryOptions<
  type AtomWithQueryResult (line 130) | type AtomWithQueryResult<
  type DefinedAtomWithQueryResult (line 135) | type DefinedAtomWithQueryResult<
  type AtomWithSuspenseQueryResult (line 140) | type AtomWithSuspenseQueryResult<
  type AtomWithInfiniteQueryResult (line 149) | type AtomWithInfiniteQueryResult<
  type DefinedAtomWithInfiniteQueryResult (line 154) | type DefinedAtomWithInfiniteQueryResult<
  type AtomWithSuspenseInfiniteQueryResult (line 159) | type AtomWithSuspenseInfiniteQueryResult<
  type UndefinedInitialDataOptions (line 166) | type UndefinedInitialDataOptions<
  type NonUndefinedGuard (line 175) | type NonUndefinedGuard<T> = T extends undefined ? never : T
  type DefinedInitialDataOptions (line 177) | type DefinedInitialDataOptions<
  type UndefinedInitialDataInfiniteOptions (line 188) | type UndefinedInitialDataInfiniteOptions<
  type DefinedInitialDataInfiniteOptions (line 204) | type DefinedInitialDataInfiniteOptions<

FILE: src/utils.ts
  function shouldThrowError (line 47) | function shouldThrowError<T extends (...args: any[]) => boolean>(
Condensed preview — 111 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (172K chars).
[
  {
    "path": ".github/workflows/cd.yml",
    "chars": 584,
    "preview": "name: CD\n\non:\n  release:\n    types: [published]\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: ac"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 391,
    "preview": "name: CI\n\non:\n  push:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - use"
  },
  {
    "path": ".github/workflows/pr.yml",
    "chars": 495,
    "preview": "name: Publish Preview\n\non:\n  pull_request:\n    types: [opened, synchronize]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n"
  },
  {
    "path": ".gitignore",
    "chars": 28,
    "preview": "*~\n*.swp\nnode_modules\n/dist\n"
  },
  {
    "path": ".prettierrc",
    "chars": 133,
    "preview": "{\n  \"semi\": false,\n  \"trailingComma\": \"es5\",\n  \"singleQuote\": true,\n  \"bracketSameLine\": true,\n  \"tabWidth\": 2,\n  \"print"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 2779,
    "preview": "# Change Log\n\n## [Unreleased]\n\n## [0.11.0] - 2025-08-01\n\n### Changed\n\n- feat: `QueryClientAtomProvider` is a ready-to-us"
  },
  {
    "path": "LICENSE",
    "chars": 1078,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2022 Daishi Kato\n\nPermission is hereby granted, free of charge, to any person obtai"
  },
  {
    "path": "README.md",
    "chars": 25166,
    "preview": "# Jotai Query 🚀 👻\n\n[jotai-tanstack-query](https://github.com/jotai-labs/jotai-tanstack-query) is a Jotai extension libra"
  },
  {
    "path": "__tests__/01_basic.spec.tsx",
    "chars": 600,
    "preview": "import {\n  atomWithInfiniteQuery,\n  atomWithMutation,\n  atomWithMutationState,\n  atomWithQuery,\n  atomWithSuspenseInfini"
  },
  {
    "path": "__tests__/atomWithInfiniteQuery.spec.tsx",
    "chars": 8890,
    "preview": "import React, { Component, StrictMode, Suspense } from 'react'\nimport type { ReactNode } from 'react'\nimport { fireEvent"
  },
  {
    "path": "__tests__/atomWithMutation.spec.tsx",
    "chars": 1423,
    "preview": "import React, { useState } from 'react'\nimport { fireEvent, render } from '@testing-library/react'\nimport { useAtom } fr"
  },
  {
    "path": "__tests__/atomWithMutationState.spec.tsx",
    "chars": 2199,
    "preview": "import React from 'react'\nimport { QueryClient } from '@tanstack/query-core'\nimport { fireEvent, render } from '@testing"
  },
  {
    "path": "__tests__/atomWithQuery.spec.tsx",
    "chars": 17803,
    "preview": "import React, { StrictMode, Suspense, useState } from 'react'\nimport { QueryClient } from '@tanstack/query-core'\nimport "
  },
  {
    "path": "__tests__/atomWithSuspenseInfiniteQuery.spec.tsx",
    "chars": 1572,
    "preview": "import React, { StrictMode, Suspense } from 'react'\nimport { fireEvent, render } from '@testing-library/react'\nimport { "
  },
  {
    "path": "__tests__/atomWithSuspenseQuery.spec.tsx",
    "chars": 13952,
    "preview": "import React, { StrictMode, Suspense } from 'react'\nimport { QueryClient } from '@tanstack/query-core'\nimport { fireEven"
  },
  {
    "path": "eslint.config.js",
    "chars": 4655,
    "preview": "import js from '@eslint/js'\nimport typescript from '@typescript-eslint/eslint-plugin'\nimport typescriptParser from '@typ"
  },
  {
    "path": "examples/01_query/index.html",
    "chars": 315,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "examples/01_query/package.json",
    "chars": 535,
    "preview": "{\n  \"name\": \"jotai-tanstack-query-example-query\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@tan"
  },
  {
    "path": "examples/01_query/src/App.tsx",
    "chars": 1058,
    "preview": "import { Getter } from 'jotai'\nimport { useAtom } from 'jotai/react'\nimport { atom } from 'jotai/vanilla'\nimport { atomW"
  },
  {
    "path": "examples/01_query/src/index.tsx",
    "chars": 204,
    "preview": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst ele = document.ge"
  },
  {
    "path": "examples/01_query/tsconfig.json",
    "chars": 605,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "examples/01_query/tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "examples/01_query/vite.config.ts",
    "chars": 133,
    "preview": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: ["
  },
  {
    "path": "examples/02_suspense/index.html",
    "chars": 315,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "examples/02_suspense/package.json",
    "chars": 538,
    "preview": "{\n  \"name\": \"jotai-tanstack-query-example-suspense\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@"
  },
  {
    "path": "examples/02_suspense/src/App.tsx",
    "chars": 1011,
    "preview": "import { Suspense } from 'react'\nimport { useAtom } from 'jotai/react'\nimport { atom } from 'jotai/vanilla'\nimport { ato"
  },
  {
    "path": "examples/02_suspense/src/index.tsx",
    "chars": 204,
    "preview": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst ele = document.ge"
  },
  {
    "path": "examples/02_suspense/tsconfig.json",
    "chars": 605,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "examples/02_suspense/tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "examples/02_suspense/vite.config.ts",
    "chars": 133,
    "preview": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: ["
  },
  {
    "path": "examples/03_infinite/index.html",
    "chars": 315,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "examples/03_infinite/package.json",
    "chars": 538,
    "preview": "{\n  \"name\": \"jotai-tanstack-query-example-infinite\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@"
  },
  {
    "path": "examples/03_infinite/src/App.tsx",
    "chars": 995,
    "preview": "import { useAtom } from 'jotai'\nimport { atomWithInfiniteQuery } from 'jotai-tanstack-query'\n\nconst postsAtom = atomWith"
  },
  {
    "path": "examples/03_infinite/src/index.tsx",
    "chars": 204,
    "preview": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst ele = document.ge"
  },
  {
    "path": "examples/03_infinite/tsconfig.json",
    "chars": 605,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "examples/03_infinite/tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "examples/03_infinite/vite.config.ts",
    "chars": 133,
    "preview": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: ["
  },
  {
    "path": "examples/04_infinite_suspense/index.html",
    "chars": 315,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "examples/04_infinite_suspense/package.json",
    "chars": 547,
    "preview": "{\n  \"name\": \"jotai-tanstack-query-example-infinite-suspense\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\":"
  },
  {
    "path": "examples/04_infinite_suspense/src/App.tsx",
    "chars": 942,
    "preview": "import { Suspense } from 'react'\nimport { useAtom } from 'jotai/react'\nimport { atomWithSuspenseInfiniteQuery } from 'jo"
  },
  {
    "path": "examples/04_infinite_suspense/src/index.tsx",
    "chars": 204,
    "preview": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst ele = document.ge"
  },
  {
    "path": "examples/04_infinite_suspense/tsconfig.json",
    "chars": 605,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "examples/04_infinite_suspense/tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "examples/04_infinite_suspense/vite.config.ts",
    "chars": 133,
    "preview": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: ["
  },
  {
    "path": "examples/05_mutation/index.html",
    "chars": 315,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "examples/05_mutation/package.json",
    "chars": 538,
    "preview": "{\n  \"name\": \"jotai-tanstack-query-example-mutation\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@"
  },
  {
    "path": "examples/05_mutation/src/App.tsx",
    "chars": 4118,
    "preview": "import React from 'react'\nimport { useAtom } from 'jotai/react'\nimport { atomWithMutation, atomWithQuery, queryClientAto"
  },
  {
    "path": "examples/05_mutation/src/index.tsx",
    "chars": 204,
    "preview": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst ele = document.ge"
  },
  {
    "path": "examples/05_mutation/tsconfig.json",
    "chars": 605,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "examples/05_mutation/tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "examples/05_mutation/vite.config.ts",
    "chars": 133,
    "preview": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: ["
  },
  {
    "path": "examples/06_refetch/index.html",
    "chars": 315,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "examples/06_refetch/package.json",
    "chars": 537,
    "preview": "{\n  \"name\": \"jotai-tanstack-query-example-refetch\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@t"
  },
  {
    "path": "examples/06_refetch/src/App.tsx",
    "chars": 1598,
    "preview": "import { useAtom } from 'jotai/react'\nimport { atom } from 'jotai/vanilla'\nimport { atomWithQuery } from 'jotai-tanstack"
  },
  {
    "path": "examples/06_refetch/src/index.tsx",
    "chars": 204,
    "preview": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst ele = document.ge"
  },
  {
    "path": "examples/06_refetch/tsconfig.json",
    "chars": 605,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "examples/06_refetch/tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "examples/06_refetch/vite.config.ts",
    "chars": 133,
    "preview": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: ["
  },
  {
    "path": "examples/07_queries/index.html",
    "chars": 315,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "examples/07_queries/package.json",
    "chars": 537,
    "preview": "{\n  \"name\": \"jotai-tanstack-query-example-queries\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@t"
  },
  {
    "path": "examples/07_queries/src/App.tsx",
    "chars": 3125,
    "preview": "import { Atom, atom } from 'jotai'\nimport { useAtom } from 'jotai/react'\nimport { type AtomWithQueryResult, atomWithQuer"
  },
  {
    "path": "examples/07_queries/src/index.tsx",
    "chars": 204,
    "preview": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst ele = document.ge"
  },
  {
    "path": "examples/07_queries/tsconfig.json",
    "chars": 605,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "examples/07_queries/tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "examples/07_queries/vite.config.ts",
    "chars": 133,
    "preview": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: ["
  },
  {
    "path": "examples/08_query_client_atom_provider/index.html",
    "chars": 315,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "examples/08_query_client_atom_provider/package.json",
    "chars": 595,
    "preview": "{\n  \"name\": \"jotai-tanstack-query-example-query-client-atom-provider\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"depen"
  },
  {
    "path": "examples/08_query_client_atom_provider/src/App.tsx",
    "chars": 2110,
    "preview": "import { QueryClient, useQuery } from '@tanstack/react-query'\nimport { useAtom } from 'jotai/react'\nimport { atom } from"
  },
  {
    "path": "examples/08_query_client_atom_provider/src/index.tsx",
    "chars": 204,
    "preview": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst ele = document.ge"
  },
  {
    "path": "examples/08_query_client_atom_provider/tsconfig.json",
    "chars": 605,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "examples/08_query_client_atom_provider/tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "examples/08_query_client_atom_provider/vite.config.ts",
    "chars": 133,
    "preview": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: ["
  },
  {
    "path": "examples/09_error_boundary/index.html",
    "chars": 315,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "examples/09_error_boundary/package.json",
    "chars": 582,
    "preview": "{\n  \"name\": \"jotai-tanstack-query-example-error-boundary\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n"
  },
  {
    "path": "examples/09_error_boundary/src/App.tsx",
    "chars": 2005,
    "preview": "import { Suspense } from 'react'\nimport { atom, useAtom, useSetAtom } from 'jotai'\nimport { atomWithSuspenseQuery } from"
  },
  {
    "path": "examples/09_error_boundary/src/index.tsx",
    "chars": 204,
    "preview": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst ele = document.ge"
  },
  {
    "path": "examples/09_error_boundary/tsconfig.json",
    "chars": 605,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "examples/09_error_boundary/tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "examples/09_error_boundary/vite.config.ts",
    "chars": 133,
    "preview": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: ["
  },
  {
    "path": "examples/11_nextjs_app_router/.gitignore",
    "chars": 480,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "examples/11_nextjs_app_router/README.md",
    "chars": 1450,
    "preview": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-re"
  },
  {
    "path": "examples/11_nextjs_app_router/eslint.config.mjs",
    "chars": 465,
    "preview": "import { defineConfig, globalIgnores } from \"eslint/config\";\nimport nextVitals from \"eslint-config-next/core-web-vitals\""
  },
  {
    "path": "examples/11_nextjs_app_router/next.config.ts",
    "chars": 133,
    "preview": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n};\n\nexport default"
  },
  {
    "path": "examples/11_nextjs_app_router/package.json",
    "chars": 668,
    "preview": "{\n  \"name\": \"jotai-tanstack-query-example-nextjs-app-router\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n  "
  },
  {
    "path": "examples/11_nextjs_app_router/postcss.config.mjs",
    "chars": 94,
    "preview": "const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "examples/11_nextjs_app_router/src/app/api.ts",
    "chars": 215,
    "preview": "export async function getPost(postId: string) {\n  console.debug('getPost called with postId:', postId)\n  const res = awa"
  },
  {
    "path": "examples/11_nextjs_app_router/src/app/layout.tsx",
    "chars": 286,
    "preview": "import type { ReactNode } from 'react'\nimport Providers from './providers'\n\nexport default function RootLayout({\n  child"
  },
  {
    "path": "examples/11_nextjs_app_router/src/app/page.tsx",
    "chars": 102,
    "preview": "import { redirect } from 'next/navigation'\n\nexport default function Home() {\n  redirect('/posts/1')\n}\n"
  },
  {
    "path": "examples/11_nextjs_app_router/src/app/posts/[postId]/_components/post.tsx",
    "chars": 1045,
    "preview": "'use client'\n\nimport { useEffect } from 'react'\nimport { useAtomValue, useSetAtom } from 'jotai'\nimport Link from 'next/"
  },
  {
    "path": "examples/11_nextjs_app_router/src/app/posts/[postId]/page.tsx",
    "chars": 786,
    "preview": "// app/posts/[postId]/page.tsx\nimport {\n  HydrationBoundary,\n  QueryClient,\n  dehydrate,\n} from '@tanstack/react-query'\n"
  },
  {
    "path": "examples/11_nextjs_app_router/src/app/posts/page.tsx",
    "chars": 720,
    "preview": "import Link from 'next/link'\n\nexport default function PostsPage() {\n  return (\n    <div>\n      Posts\n      <div>\n       "
  },
  {
    "path": "examples/11_nextjs_app_router/src/app/providers.tsx",
    "chars": 2082,
    "preview": "// In Next.js, this file would be called: app/providers.tsx\n'use client'\n\n// Since QueryClientProvider relies on useCont"
  },
  {
    "path": "examples/11_nextjs_app_router/src/app/stores.ts",
    "chars": 405,
    "preview": "import { atom } from 'jotai'\nimport { atomWithQuery } from 'jotai-tanstack-query'\nimport { getPost } from './api'\n\nexpor"
  },
  {
    "path": "examples/11_nextjs_app_router/tsconfig.json",
    "chars": 678,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  },
  {
    "path": "package.json",
    "chars": 3463,
    "preview": "{\n  \"name\": \"jotai-tanstack-query\",\n  \"description\": \"👻🌺\",\n  \"version\": \"0.11.0\",\n  \"type\": \"module\",\n  \"author\": \"Daish"
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 163,
    "preview": "linkWorkspacePackages: true\npreferWorkspacePackages: true\npreferFrozenLockfile: true\n\npackages:\n  - examples/*\n\nonlyBuil"
  },
  {
    "path": "src/_queryClientAtom.ts",
    "chars": 230,
    "preview": "import { QueryClient } from '@tanstack/query-core'\nimport { atom } from 'jotai/vanilla'\n\nexport const queryClientAtom = "
  },
  {
    "path": "src/atomWithInfiniteQuery.ts",
    "chars": 2182,
    "preview": "import {\n  type DefaultError,\n  type InfiniteData,\n  InfiniteQueryObserver,\n  QueryClient,\n  type QueryKey,\n  QueryObser"
  },
  {
    "path": "src/atomWithMutation.ts",
    "chars": 3041,
    "preview": "import {\n  MutationObserver,\n  type MutationOptions,\n  QueryClient,\n  notifyManager,\n} from '@tanstack/query-core'\nimpor"
  },
  {
    "path": "src/atomWithMutationState.ts",
    "chars": 1719,
    "preview": "import {\n  type DefaultError,\n  Mutation,\n  MutationCache,\n  type MutationFilters,\n  type MutationState,\n  QueryClient,\n"
  },
  {
    "path": "src/atomWithQueries.ts",
    "chars": 1670,
    "preview": "import {\n  type DefaultError,\n  QueryClient,\n  QueryObserver,\n} from '@tanstack/query-core'\nimport { Getter, WritableAto"
  },
  {
    "path": "src/atomWithQuery.ts",
    "chars": 1768,
    "preview": "import {\n  type DefaultError,\n  QueryClient,\n  type QueryKey,\n  QueryObserver,\n} from '@tanstack/query-core'\nimport { Ge"
  },
  {
    "path": "src/atomWithSuspenseInfiniteQuery.ts",
    "chars": 1438,
    "preview": "import {\n  type DefaultError,\n  type InfiniteData,\n  InfiniteQueryObserver,\n  QueryClient,\n  type QueryKey,\n  QueryObser"
  },
  {
    "path": "src/atomWithSuspenseQuery.ts",
    "chars": 1213,
    "preview": "import {\n  type DefaultError,\n  QueryClient,\n  type QueryKey,\n  QueryObserver,\n} from '@tanstack/query-core'\nimport { Ge"
  },
  {
    "path": "src/baseAtomWithQuery.ts",
    "chars": 3677,
    "preview": "import {\n  QueryClient,\n  type QueryKey,\n  QueryObserver,\n  type QueryObserverResult,\n  notifyManager,\n} from '@tanstack"
  },
  {
    "path": "src/index.ts",
    "chars": 503,
    "preview": "export { queryClientAtom } from './_queryClientAtom'\nexport { atomWithQuery } from './atomWithQuery'\nexport { atomWithQu"
  },
  {
    "path": "src/react.ts",
    "chars": 685,
    "preview": "import { createElement } from 'react'\nimport {\n  QueryClientProvider,\n  type QueryClientProviderProps,\n} from '@tanstack"
  },
  {
    "path": "src/types.ts",
    "chars": 5452,
    "preview": "import {\n  type DefaultError,\n  type DefinedInfiniteQueryObserverResult,\n  type DefinedQueryObserverResult,\n  type Infin"
  },
  {
    "path": "src/utils.ts",
    "chars": 1978,
    "preview": "import type {\n  DefaultError,\n  DefaultedQueryObserverOptions,\n  Query,\n  QueryKey,\n  QueryObserverResult,\n  ThrowOnErro"
  },
  {
    "path": "tsconfig.json",
    "chars": 597,
    "preview": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"target\": \"esnext\",\n    \"downlevelIteration\": true,\n    \"esModuleIntero"
  },
  {
    "path": "vitest.config.ts",
    "chars": 471,
    "preview": "/// <reference types=\"vitest\" />\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n "
  }
]

About this extraction

This page contains the full source code of the jotaijs/jotai-tanstack-query GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 111 files (153.1 KB), approximately 45.4k tokens, and a symbol index with 57 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!