diff --git a/docs/integrations/query.mdx b/docs/integrations/query.mdx
index 7e02d8d1a2..896b35e250 100644
--- a/docs/integrations/query.mdx
+++ b/docs/integrations/query.mdx
@@ -1,37 +1,58 @@
---
-title: Query
-description: This doc describes `jotai/query` bundle.
+title: TanStack Query
+description: This doc describes TanStack Query integration.
nav: 4.03
---
-This is the Jotai integration with [TanStack Query v4](https://tanstack.com/query/v4), a set of hooks and tools for managing async state (typically external data).
+[TanStack Query](https://tanstack.com/query/) provides a set of functions for managing async state (typically external data).
From the [Overview docs](https://tanstack.com/query/v4/docs/overview):
> React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes **fetching, caching, synchronizing and updating server state** in your React applications a breeze.
-Jotai with TanStack Query 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.
+[jotai-tanstack-query](https://github.com/jotai-labs/jotai-tanstack-query) is a Jotai integration 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.
## Install
-In addition to `jotai`, you have to install `@tanstack/query-core` to make use of this bundle and its functions.
+In addition to `jotai`, you have to install `jotai-tanstack-query` and `@tanstack/query-core` to use the integration.
```bash
-yarn add @tanstack/query-core
+yarn add jotai-tanstack-query @tanstack/query-core
```
-## atomWithQuery
+## Exported functions
-`atomWithQuery` creates a new atom that implements a standard [`Query`](https://tanstack.com/query/v4/docs/guides/queries) from TanStack Query. This function combines both Jotai `atom` features and `useQuery` features in a single atom. This atom supports all features you'd expect to find when using the standard `@tanstack/react-query` package.
+- `atomsWithQuery` for [QueryObserver](https://tanstack.com/query/v4/docs/reference/QueryObserver)
+- `atomsWithInfiniteQuery` for [InfiniteQueryObserver](https://tanstack.com/query/v4/docs/reference/InfiniteQueryObserver)
+- `atomsWithMutation` for MutationObserver
+
+All three functions follow the same signature.
+
+```ts
+const [dataAtom, statusAtom] = atomsWithSomething(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/v4/docs/reference/QueryClient).
+
+The return values have two atoms.
+The first one is called `dataAtom` and it's an atom for the data from the observer. `dataAtom` requires Suspense.
+The second one is called `statusAtom` and it's an atom for the full result from the observer. `statusAtom` doesn't require Suspnse.
+The data from the observer is also included in `statusAtom`,
+so if you don't use Suspense, you don't need to use `dataAtom`.
+
+## `atomsWithQuery` usage
+
+`atomsWithQuery` creates new atoms that implement a standard [`Query`](https://tanstack.com/query/v4/docs/guides/queries) from TanStack Query.
> A query is a declarative dependency on an asynchronous source of data that is tied to a unique key. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server.
```jsx
import { useAtom } from 'jotai'
-import { atomWithQuery } from 'jotai/query'
+import { atomsWithQuery } from 'jotai-tanstack-query'
const idAtom = atom(1)
-const userAtom = atomWithQuery((get) => ({
+const [userAtom] = atomsWithQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
@@ -45,9 +66,9 @@ const UserData = () => {
}
```
-## atomWithInfiniteQuery
+## `atomsWithTansackInfiniteQuery` usage
-`atomWithInfiniteQuery` is very similar to `atomWithQuery`, however it creates 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/v4/docs/guides/infinite-queries). This atom supports all features you'd expect to find when using the standard `@tanstack/react-query` package.
+`atomsWithInfiniteQuery` is very similar to `atomsWithQuery`, 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/v4/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.
@@ -55,10 +76,10 @@ A notable difference between a standard query atom is the additional option `get
```jsx
import { useAtom } from 'jotai'
-import { atomWithInfiniteQuery } from 'jotai/query'
+import { atomsWithInfiniteQuery } from 'jotai-tanstack-query'
const idAtom = atom(1)
-const userAtom = atomWithInfiniteQuery((get) => ({
+const [userAtom] = atomsWithInfiniteQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
@@ -78,17 +99,50 @@ const UserData = () => {
### Fetching pages and refetching
-Using the same atom as in the above example, we can use the `update` method returned from `useAtom` to call different `@tanstack/react-query` methods:
+Using the same atom as in the above example, we can dispatch an action to `userAtom`.
```js
const UserData = () => {
- const [data, update] = useAtom(userAtom)
+ const [data, dispatch] = useAtom(userAtom)
+ const handleFetchNextPage = () => dispatch({ type: 'fetchNextPage' })
+ const handleFetchPreviousPage = () => dispatch({ type: 'fetchPreviousPage' })
+}
+```
- const handleFetchNextPage = () => update({ type: 'fetchNextPage' })
+## `atomsWithTansackMutation` usage
- const handleFetchPreviousPage = () => update({ type: 'fetchPreviousPage' })
+`atomsWithMutation` creates new atoms that implement a standard [`Mutatioin`](https://tanstack.com/query/v4/docs/guides/mutations) from TanStack Query.
- const handleRefetchAllPages = () => update({ type: 'refetch' })
+> Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects.
+
+```jsx
+import { useAtom } from 'jotai'
+import { atomsWithMutation } from 'jotai-tanstack-query'
+
+const idAtom = atom(1)
+const [, postAtom] = atomsWithMutation((get) => ({
+ mutationKey: ['posts'],
+ mutationFn: async ({ title, body }) => {
+ const res = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
+ method: 'POST',
+ body: JSON.stringify({ title, body, userId: get(idAtom) }),
+ headers: { 'Content-type': 'application/json; charset=UTF-8' },
+ })
+ const data = await res.json()
+ return data
+ },
+}))
+
+const PostData = () => {
+ const [post, mutate] = useAtom(postAtom)
+ return (
+
+
+
{JSON.stringify(post)}
+
+ )
}
```
@@ -100,6 +154,8 @@ To ensure that you reference the same `QueryClient` object, be sure to wrap the
Without this step, `useQueryAtom` will reference a seperate `QueryClient` from any hooks that utilise the `useQueryClient()` hook to get the queryClient.
+Alternatively, you can specify your `queryClient` with `getQueryClient` parameter.
+
### Example
In the example below, we have a mutation hook, `useTodoMutation` and a query `todosAtom`.
@@ -117,7 +173,7 @@ import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
-import { atomWithQuery, queryClientAtom } from 'jotai/query'
+import { atomsWithQuery, queryClientAtom } from 'jotai-tanstack-query'
const queryClient = new QueryClient()
export const App = () => {
@@ -133,7 +189,7 @@ export const App = () => {
)
}
-export const todosAtom = atomWithQuery((get) => {
+export const [todosAtom] = atomsWithQuery((get) => {
return {
queryKey: ['todos'],
queryFn: () => fetch('/todos'),
@@ -159,21 +215,28 @@ export const useTodoMutation = () => {
## SSR support
-Both 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/v4/docs/guides/ssr) that React Query supports for use within SSR apps, [hydration](https://tanstack.com/query/v4/docs/guides/ssr#using-hydration) or [`initialData`](https://tanstack.com/query/v4/docs/guides/ssr#using-initialdata).
+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/v4/docs/guides/ssr) that React Query supports for use within SSR apps, [hydration](https://tanstack.com/query/v4/docs/guides/ssr#using-hydration) or [`initialData`](https://tanstack.com/query/v4/docs/guides/ssr#using-initialdata).
## Error handling
+With `dataAtom`,
Fetch error will be thrown and can be caught with ErrorBoundary.
Refetching may recover from a temporary error.
+FIXME: Update example
+
See [a working example](https://codesandbox.io/s/04mepx) to learn more.
## Examples
### Basic demo
+FIXME: Update demo
+
### Hackernews
+FIXME: Update demo
+
diff --git a/package.json b/package.json
index b4763b0468..cf0a809917 100644
--- a/package.json
+++ b/package.json
@@ -238,6 +238,7 @@
"immer": "^9.0.15",
"jest": "^29.2.0",
"jest-environment-jsdom": "^29.2.0",
+ "jotai-tanstack-query": "^0.4.0",
"json": "^11.0.0",
"optics-ts": "^2.3.0",
"postinstall-postinstall": "^2.1.0",
@@ -265,9 +266,9 @@
"peerDependencies": {
"@babel/core": "*",
"@babel/template": "*",
- "@tanstack/query-core": "*",
"@urql/core": "*",
"immer": "*",
+ "jotai-tanstack-query": "*",
"optics-ts": "*",
"react": ">=16.8",
"valtio": "*",
@@ -287,10 +288,10 @@
"immer": {
"optional": true
},
- "optics-ts": {
+ "jotai-tanstack-query": {
"optional": true
},
- "@tanstack/query-core": {
+ "optics-ts": {
"optional": true
},
"valtio": {
diff --git a/src/query/atomWithInfiniteQuery.ts b/src/query/atomWithInfiniteQuery.ts
index bd3f19fb57..846cb309c8 100644
--- a/src/query/atomWithInfiniteQuery.ts
+++ b/src/query/atomWithInfiniteQuery.ts
@@ -1,14 +1,13 @@
-import { InfiniteQueryObserver, isCancelledError } from '@tanstack/query-core'
import type {
InfiniteData,
InfiniteQueryObserverOptions,
QueryKey,
- QueryObserverResult,
RefetchOptions,
RefetchQueryFilters,
} from '@tanstack/query-core'
+import { atomsWithInfiniteQuery } from 'jotai-tanstack-query'
import { atom } from 'jotai'
-import type { WritableAtom } from 'jotai'
+import type { Getter, WritableAtom } from 'jotai'
import { queryClientAtom } from './queryClientAtom'
import { CreateQueryOptions, GetQueryClient } from './types'
@@ -117,113 +116,29 @@ export function atomWithInfiniteQuery<
InfiniteData | undefined,
AtomWithInfiniteQueryAction
> {
- type Result = QueryObserverResult, TError>
- type State = {
- isMounted: boolean
- unsubscribe: (() => void) | null
- }
- const queryDataAtom = atom(
+ const getOptions = (get: Getter) => ({
+ staleTime: 200,
+ ...(typeof createQuery === 'function' ? createQuery(get) : createQuery),
+ })
+ const [dataAtom] = atomsWithInfiniteQuery(getOptions, getQueryClient)
+ return atom(
(get) => {
- const queryClient = getQueryClient(get)
- const options =
- typeof createQuery === 'function' ? createQuery(get) : createQuery
-
- const defaultedOptions = queryClient.defaultQueryOptions(options)
- const observer = new InfiniteQueryObserver(queryClient, defaultedOptions)
- const initialResult = observer.getCurrentResult()
-
- let resolve: ((result: Result) => void) | null = null
- const resultAtom = atom>(
- initialResult.data === undefined && options.enabled !== false
- ? new Promise((r) => {
- resolve = r
- })
- : initialResult
- )
- let setResult: (result: Result) => void = () => {
- throw new Error('setting result without mount')
- }
- const state: State = {
- isMounted: false,
- unsubscribe: null,
- }
- const listener = (result: Result) => {
- if (
- result.isFetching ||
- (!result.isError && result.data === undefined) ||
- (result.isError && isCancelledError(result.error))
- ) {
- return
- }
- if (resolve) {
- setTimeout(() => {
- if (!state.isMounted) {
- state.unsubscribe?.()
- state.unsubscribe = null
- }
- }, 1000)
- resolve(result)
- resolve = null
- } else {
- setResult(result)
- }
- }
- if (options.enabled !== false) {
- state.unsubscribe = observer.subscribe(listener)
- }
- resultAtom.onMount = (update) => {
- setResult = update
- state.isMounted = true
- if (options.enabled !== false && !state.unsubscribe) {
- state.unsubscribe = observer.subscribe(listener)
- listener(observer.getCurrentResult())
- }
- return () => state.unsubscribe?.()
- }
- return { options, resultAtom, observer, state }
- },
- (get, set, action: AtomWithInfiniteQueryAction) => {
- const { options, resultAtom, observer, state } = get(queryDataAtom)
+ const options = getOptions(get)
if (options.enabled === false) {
- return
+ const queryClient = getQueryClient(get)
+ return queryClient.getQueryData>(options.queryKey)
}
- switch (action.type) {
- case 'refetch': {
- set(resultAtom, new Promise(() => {})) // infinite pending
- if (!state.isMounted) {
- state.unsubscribe?.()
- state.unsubscribe = null
- }
- observer.refetch(action.payload).then((result) => {
- set(resultAtom, result)
- })
- return
- }
- case 'fetchPreviousPage': {
- observer.fetchPreviousPage()
- return
- }
- case 'fetchNextPage': {
- observer.fetchNextPage()
- return
- }
+ return get(dataAtom)
+ },
+ (_get, set, action: AtomWithInfiniteQueryAction) => {
+ if (action.type === 'refetch') {
+ return set(dataAtom, {
+ type: 'refetch',
+ force: true,
+ options: action.payload as any,
+ })
}
+ return set(dataAtom, action)
}
)
-
- const queryAtom = atom<
- InfiniteData | undefined,
- AtomWithInfiniteQueryAction
- >(
- (get) => {
- const { resultAtom } = get(queryDataAtom)
- const result = get(resultAtom)
- if (result.isError) {
- throw result.error
- }
- return result.data
- },
- (_get, set, action) => set(queryDataAtom, action) // delegate action
- )
- return queryAtom
}
diff --git a/src/query/atomWithQuery.ts b/src/query/atomWithQuery.ts
index 4be8367623..40d9e0d5d7 100644
--- a/src/query/atomWithQuery.ts
+++ b/src/query/atomWithQuery.ts
@@ -1,16 +1,10 @@
-import { QueryClient, QueryObserver } from '@tanstack/query-core'
-import type {
- QueryKey,
- QueryObserverOptions,
- QueryObserverResult,
-} from '@tanstack/query-core'
+import type { QueryKey, QueryObserverOptions } from '@tanstack/query-core'
+import { atomsWithQuery } from 'jotai-tanstack-query'
import { atom } from 'jotai'
-import type { WritableAtom } from 'jotai'
+import type { Getter, WritableAtom } from 'jotai'
import { queryClientAtom } from './queryClientAtom'
import type { CreateQueryOptions, GetQueryClient } from './types'
-type Timeout = ReturnType
-
type AtomWithQueryAction = {
type: 'refetch'
}
@@ -88,119 +82,24 @@ export function atomWithQuery<
>,
getQueryClient: GetQueryClient = (get) => get(queryClientAtom)
): WritableAtom> {
- type Result = QueryObserverResult
-
- // HACK: having state in atom creator function is not ideal
- // because, unlike hooks, it's shared by multiple providers.
- const previousDataCache = new WeakMap()
-
- const queryDataAtom = atom((get) => {
- const queryClient = getQueryClient(get)
- const options =
- typeof createQuery === 'function' ? createQuery(get) : createQuery
- const observer = new QueryObserver(queryClient, options)
- const initialResult = observer.getCurrentResult()
- if (
- initialResult.data === undefined &&
- options.keepPreviousData &&
- previousDataCache.has(queryClient)
- ) {
- initialResult.data = previousDataCache.get(queryClient)
- }
-
- let resolve: ((result: Result) => void) | null = null
- const makePending = () =>
- new Promise((r) => {
- resolve = r
- })
- const resultAtom = atom>(
- initialResult.data === undefined && options.enabled !== false
- ? makePending()
- : initialResult
- )
- let setResult: ((result: Result) => void) | null = null
- const listener = (result: Result) => {
- if (result.isFetching || (!result.isError && result.data === undefined)) {
- return
- }
- if (!resolve && !setResult) {
- throw new Error('setting result without mount')
- }
- if (resolve) {
- resolve(result)
- resolve = null
- }
- if (setResult) {
- setResult(result)
- }
- if (result.data !== undefined) {
- previousDataCache.set(queryClient, result.data)
- }
- }
- let unsubscribe: (() => void) | null = null
- let timer: Timeout | undefined
- const startQuery = () => {
- if (!setResult && unsubscribe) {
- clearTimeout(timer)
- unsubscribe()
- unsubscribe = null
- }
- if (options.enabled !== false) {
- if (setResult) {
- observer.refetch({ cancelRefetch: true }).then(setResult)
- } else {
- unsubscribe = observer.subscribe(listener)
- }
- }
- if (!setResult) {
- // not mounted yet
- timer = setTimeout(() => {
- if (unsubscribe) {
- unsubscribe()
- unsubscribe = null
- }
- }, 1000)
- }
- }
- startQuery()
- resultAtom.onMount = (update) => {
- setResult = update
- if (unsubscribe) {
- clearTimeout(timer)
- } else {
- startQuery()
- }
- return () => {
- setResult = null
- if (unsubscribe) {
- unsubscribe()
- // FIXME why does this fail?
- // unsubscribe = null
- }
- }
- }
- return { resultAtom, makePending, startQuery }
+ const getOptions = (get: Getter) => ({
+ staleTime: 200,
+ ...(typeof createQuery === 'function' ? createQuery(get) : createQuery),
})
-
- const queryAtom = atom(
+ const [dataAtom] = atomsWithQuery(getOptions, getQueryClient)
+ return atom(
(get) => {
- const { resultAtom } = get(queryDataAtom)
- const result = get(resultAtom)
- if (result.isError) {
- throw result.error
+ const options = getOptions(get)
+ if (options.enabled === false) {
+ const queryClient = getQueryClient(get)
+ return queryClient.getQueryData(options.queryKey)
}
- return result.data
+ return get(dataAtom)
},
- (get, set, action: AtomWithQueryAction) => {
- const { resultAtom, makePending, startQuery } = get(queryDataAtom)
- switch (action.type) {
- case 'refetch': {
- set(resultAtom, makePending())
- startQuery()
- }
+ (_get, set, action: AtomWithQueryAction) => {
+ if (action.type === 'refetch') {
+ return set(dataAtom, { type: 'refetch', force: true })
}
}
)
-
- return queryAtom
}
diff --git a/src/query/queryClientAtom.ts b/src/query/queryClientAtom.ts
index 98c09a7153..748f6ff0ea 100644
--- a/src/query/queryClientAtom.ts
+++ b/src/query/queryClientAtom.ts
@@ -1,4 +1 @@
-import { QueryClient } from '@tanstack/query-core'
-import { atom } from 'jotai'
-
-export const queryClientAtom = atom(new QueryClient())
+export { queryClientAtom } from 'jotai-tanstack-query'
diff --git a/tests/query/atomWithInfiniteQuery.test.tsx b/tests/query/atomWithInfiniteQuery.test.tsx
index 9d5f9615e7..132935485e 100644
--- a/tests/query/atomWithInfiniteQuery.test.tsx
+++ b/tests/query/atomWithInfiniteQuery.test.tsx
@@ -239,6 +239,8 @@ it('infinite query with enabled 2', async () => {
await new Promise((r) => setTimeout(r, 100))
fireEvent.click(getByText('set disabled'))
fireEvent.click(getByText('set slug'))
+
+ await new Promise((r) => setTimeout(r, 100))
await findByText('slug: hello-first')
await new Promise((r) => setTimeout(r, 100))
diff --git a/tests/query/atomWithQuery.test.tsx b/tests/query/atomWithQuery.test.tsx
index e1c56309bc..11a092e78c 100644
--- a/tests/query/atomWithQuery.test.tsx
+++ b/tests/query/atomWithQuery.test.tsx
@@ -131,7 +131,6 @@ it('query refetch', async () => {
await new Promise((r) => setTimeout(r, 100))
fireEvent.click(getByText('refetch'))
-
await findByText('loading')
await findByText('count: 1')
expect(mockFetch).toBeCalledTimes(2)
@@ -418,6 +417,7 @@ it('query with enabled 2', async () => {
expect(mockFetch).toHaveBeenCalledTimes(1)
await findByText('slug: hello-first')
+ await new Promise((r) => setTimeout(r, 100))
fireEvent.click(getByText('set disabled'))
fireEvent.click(getByText('set slug'))
@@ -517,11 +517,11 @@ it('query with initialData test', async () => {
}
const { findByText } = render(
- <>
+
- >
+
)
// NOTE: the atom never suspends
@@ -655,7 +655,6 @@ describe('error handling', () => {
const countAtom = atomWithQuery(() => ({
queryKey: ['error test', 'count2'],
retry: false,
- staleTime: 200,
queryFn: () => {
const promise = fakeFetch({ count }, willThrowError, 200)
willThrowError = !willThrowError
diff --git a/yarn.lock b/yarn.lock
index 02e58e67bc..bd7cdbc1f0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3908,6 +3908,11 @@ jest@^29.2.0:
import-local "^3.0.2"
jest-cli "^29.2.0"
+jotai-tanstack-query@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/jotai-tanstack-query/-/jotai-tanstack-query-0.4.0.tgz#f8568160efb76f884156dc61a584fabafa5c5651"
+ integrity sha512-K3++3iIKGm34hyHm0i+8cMM7c2agDq/GdE7xJ2Asa+VwyEBQVRudfdGEv3HCYLp6smNcqWUxn7iQddue/DXe8g==
+
joycon@^3.0.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"