
Security News
npm Adopts OIDC for Trusted Publishing in CI/CD Workflows
npm now supports Trusted Publishing with OIDC, enabling secure package publishing directly from CI/CD workflows without relying on long-lived tokens.
jotai-tanstack-query
Advanced tools
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.
jotai-tanstack-query currently supports Jotai v2 and TanStack Query v5.
npm i jotai jotai-tanstack-query @tanstack/react-query
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>
}
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.
// 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)
QueryClientAtomProvider
is a ready-to-use wrapper that combines Jotai Provider and TanStack Query QueryClientProvider.
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.
- 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>
)
}
atomWithQuery
for useQueryatomWithQueries
for useQueriesatomWithInfiniteQuery
for useInfiniteQueryatomWithMutation
for useMutationatomWithSuspenseQuery
for useSuspenseQueryatomWithSuspenseInfiniteQuery
for useSuspenseInfiniteQueryatomWithMutationState
for useMutationStateAll functions follow the same signature.
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.
atomWithQuery
creates a new atom that implements a standard Query
from TanStack Query.
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
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.
There are two ways to use atomWithQueries
:
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>
)
}
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
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.
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.
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
creates a new atom that implements a standard Mutation
from TanStack Query.
Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects.
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
creates a new atom that gives you access to all mutations in the MutationCache
.
const mutationStateAtom = atomWithMutationState((get) => ({
filters: {
mutationKey: ['posts'],
},
}))
jotai-tanstack-query can also be used with React's Suspense.
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>
}
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>
</>
)
}
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 that React Query supports for use within SSR apps, hydration or initialData
.
Fetch error will be thrown and can be caught with ErrorBoundary. Refetching may recover from a temporary error.
See a working example to learn more.
In order to use the Devtools, you need to install it additionally.
$ npm i @tanstack/react-query-devtools --save-dev
All you have to do is put the <ReactQueryDevtools />
within <QueryClientAtomProvider />
.
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>
)
}
queryKey
type is always unknown
?Explicitly declare the get:Getter
to get proper type inference for queryKey
.
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
},
}))
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
toatomWithSomething
.
- const [dataAtom, statusAtom] = atomsWithSomething(getOptions, getQueryClient)
+ const dataAtom = atomWithSomething(getOptions, getQueryClient)
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.
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.
- const [dataAtom, statusAtom] = atomsWithQuery(/* ... */);
- const [data] = useAtom(dataAtom);
- const [status] = useAtom(statusAtom);
+ const dataAtom = atomWithQuery(/* ... */);
+ const [{ data, isPending, isError }] = useAtom(dataAtom);
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>
.
- 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
[0.11.0] - 2025-08-01
QueryClientAtomProvider
is a ready-to-use wrapper that combines Jotai Provider and TanStack Query QueryClientProvider.QueryClientProvider
FAQs
The npm package jotai-tanstack-query receives a total of 62,716 weekly downloads. As such, jotai-tanstack-query popularity was classified as popular.
We found that jotai-tanstack-query demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
npm now supports Trusted Publishing with OIDC, enabling secure package publishing directly from CI/CD workflows without relying on long-lived tokens.
Research
/Security News
A RubyGems malware campaign used 60 malicious packages posing as automation tools to steal credentials from social media and marketing tool users.
Security News
The CNA Scorecard ranks CVE issuers by data completeness, revealing major gaps in patch info and software identifiers across thousands of vulnerabilities.