jotai-tanstack-query
Version:
564 lines (427 loc) • 18.5 kB
Markdown
# 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)
- [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 { useAtom } 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 }] = useAtom(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
// existing useQueryHook
const { data, isPending, isError } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
// jotai-tanstack-query
const todosAtom = atomWithQuery(() => ({
queryKey: ['todos'],
}))
const [{ data, isPending, isError }] = useAtom(todosAtom)
```
### Exported provider
[](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.
```diff
- import { QueryClient } from '@tanstack/react-query'
- import { QueryClientAtomProvider } from 'jotai-tanstack-query/react'
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+ import { Provider } from 'jotai/react'
+ import { useHydrateAtoms } from 'jotai/react/utils'
+ import { queryClientAtom } from 'jotai-tanstack-query'
const queryClient = new QueryClient()
+ const HydrateAtoms = ({ children }) => {
+ useHydrateAtoms([[queryClientAtom, queryClient]])
+ return children
+ }
export const Root = () => {
return (
- <QueryClientAtomProvider client={queryClient}>
+ <QueryClientProvider client={queryClient}>
+ <Provider>
+ <HydrateAtoms>
<App />
+ </HydrateAtoms>
+ </Provider>
+ </QueryClientProvider>
- </QueryClientAtomProvider>
)
}
```
### 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
[](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
[](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, useAtom } from 'jotai'
import { atomWithQueries } from 'jotai-tanstack-query'
const userIdsAtom = atom([1, 2, 3])
const UsersData = () => {
const [userIds] = useAtom(userIdsAtom)
const userQueryAtoms = 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()
},
})),
})
return (
<div>
{userQueryAtoms.map((queryAtom, index) => (
<UserData key={index} queryAtom={queryAtom} />
))}
</div>
)
}
const UserData = ({ queryAtom }) => {
const [{ data, isPending, isError }] = useAtom(queryAtom)
if (isPending) return <div>Loading...</div>
if (isError) return <div>Error</div>
return (
<div>
{data.name} - {data.email}
</div>
)
}
```
#### Advanced usage - Combine multiple query results
```jsx
import { atom, useAtom } from 'jotai'
import { atomWithQueries } from 'jotai-tanstack-query'
const userIdsAtom = atom([1, 2, 3])
const CombinedUsersData = () => {
const [userIds] = useAtom(userIdsAtom)
const combinedUsersDataAtom = 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),
}),
})
const [{ data, isPending, isError }] = useAtom(combinedUsersDataAtom)
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
[](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
[](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.
```tsx
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, status }] = useAtom(postAtom)
return (
<div>
<button onClick={() => mutate({ title: 'foo' })}>Click me</button>
<pre>{JSON.stringify(status, null, 2)}</pre>
</div>
)
}
```
### 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
[](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
[](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
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/guides/ssr) that React Query supports for use within SSR apps, [hydration](https://tanstack.com/query/v5/docs/react/guides/ssr#using-the-hydration-apis) or [`initialData`](https://tanstack.com/query/v5/docs/react/guides/ssr#get-started-fast-with-initialdata).
### 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
},
}))
```
## 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
```