JotaiJotai

ηŠΆζ…‹
Primitive and flexible state management for React

Query

TanStack Query provides a set of functions for managing async state (typically external data).

From the Overview docs:

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

Install

In addition to jotai, you have to install jotai-tanstack-query and @tanstack/query-core to use the extension.

yarn add jotai-tanstack-query @tanstack/query-core

Exported functions

All three functions follow the same signature.

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.

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 Suspense and won't throw errors to Error Boundary. The data from the observer is also included in statusAtom, so if you don't use Suspense and Error Boundary, you don't need to use dataAtom.

atomsWithQuery usage

atomsWithQuery creates new atoms that implement a standard Query 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.

import { atom, useAtom } from 'jotai'
import { atomsWithQuery } from 'jotai-tanstack-query'
const idAtom = atom(1)
const [userAtom] = atomsWithQuery((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>
}

atomsWithInfiniteQuery usage

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.

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 { atomsWithInfiniteQuery } from 'jotai-tanstack-query'
const idAtom = atom(1)
const [userAtom] = atomsWithInfiniteQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
return res.json()
},
// infinite queries can support paginated fetching
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
}))
const UserData = () => {
const [data] = useAtom(userAtom)
return data.pages.map((userData, index) => (
<div key={index}>{JSON.stringify(userData)}</div>
))
}

Fetching pages and refetching

Using the same atom as in the above example, we can dispatch an action to userAtom.

const UserData = () => {
const [data, dispatch] = useAtom(userAtom)
const handleFetchNextPage = () => dispatch({ type: 'fetchNextPage' })
const handleFetchPreviousPage = () => dispatch({ type: 'fetchPreviousPage' })
}

atomsWithMutation usage

atomsWithMutation creates new atoms that implement a standard Mutation from TanStack Query.

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

import { atom, 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 (
<div>
<button onClick={() => mutate([{ title: 'foo', body: 'bar' }])}>
Click me
</button>
<div>{JSON.stringify(post)}</div>
</div>
)
}

Referencing the same instance of Query Client in your project

Perhaps you have some custom hooks in your project that utilises the useQueryClient() hook to obtain the QueryClient object and call its methods.

To ensure that you reference the same QueryClient object, be sure to wrap the root of your project in a <Provider> and initialise queryClientAtom with the same queryClient value you provided to QueryClientProvider.

Without this step, useQueryAtom will reference a separate 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.

We included an initialisation step in our root <App> node.

Although they reference methods same query key ('todos'), the onSuccess invalidation in useTodoMutation will not trigger if the Provider initialisation step was not done.

This will result in todosAtom showing stale data as it was not prompted to refetch.

import { Provider } from 'jotai/react'
import { useHydrateAtoms } from 'jotai/react/utils'
import {
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { atomsWithQuery, queryClientAtom } from 'jotai-tanstack-query'
const queryClient = new QueryClient()
const HydrateAtoms = ({ children }) => {
useHydrateAtoms([[queryClientAtom, queryClient]])
return children
}
export const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Provider>
{/*
This Provider initialisation step is needed so that we reference the same
queryClient in both atomWithQuery and other parts of the app. Without this,
our useQueryClient() hook will return a different QueryClient object
*/}
<HydrateAtoms>
<App />
</HydrateAtoms>
</Provider>
</QueryClientProvider>
)
}
export const [todosAtom] = atomsWithQuery((get) => {
return {
queryKey: ['todos'],
queryFn: () => fetch('/todos'),
}
})
export const useTodoMutation = () => {
const queryClient = useQueryClient()
return useMutation(
async (body: todo) => {
await fetch('/todo', { Method: 'POST', Body: body })
},
{
onSuccess: () => {
void queryClient.invalidateQueries(['todos'])
},
onError,
}
)
}

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 that React Query supports for use within SSR apps, hydration or initialData.

Error handling

With dataAtom, 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.

Devtools

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

$ npm i @tanstack/react-query-devtools
# or
$ pnpm add @tanstack/react-query-devtools
# or
$ yarn add @tanstack/react-query-devtools

All you have to do is put the <ReactQueryDevtools /> in the <QueryClientProvider />.

import {
QueryClientProvider,
QueryClient,
QueryCache,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClientAtom } from 'jotai-tanstack-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
},
},
})
const HydrateAtoms = ({ children }) => {
useHydrateAtoms([[queryClientAtom, queryClient]])
return children
}
export const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Provider>
<HydrateAtoms>
<App />
</HydrateAtoms>
</Provider>
<ReactQueryDevtools />
</QueryClientProvider>
)
}

Examples

Basic demo

Devtools demo

Hackernews