Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [Incremental Adoption](#incremental-adoption)
- [Exported Functions](#exported-functions)
- [atomWithQuery](#atomwithquery-usage)
- [atomWithQueries](#atomwithqueries-usage)
- [atomWithInfiniteQuery](#atomwithinfinitequery-usage)
- [atomWithMutation](#atomwithmutation-usage)
- [atomWithMutationState](#atomwithmutationstate-usage)
Expand Down Expand Up @@ -56,6 +57,7 @@ You can incrementally adopt `jotai-tanstack-query` in your app. It's not an all
### 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)
Expand Down Expand Up @@ -98,6 +100,103 @@ const UserData = () => {
}
```

### atomWithQueries usage

`atomWithQueries` creates a new atom that implements parallel queries from TanStack Query. It allows you to run multiple queries concurrently and optionally combine their results.

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()
},
})),
})
Comment on lines +118 to +130
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerning about the example here, seems like this atom is rely on react hook? can we change it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@himself65 The atomWithQueries does not necessarily depend on React hooks; here it is used simply for convenient state management, same as useState.

Copy link
Member

@himself65 himself65 Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can change to

 const userQueryAtoms = atomWithQueries({
    queries: get(userIdsAtom).map(...)
  })

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood


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

`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).
Expand Down
2 changes: 1 addition & 1 deletion examples/01_typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"@tanstack/query-core": "latest",
"jotai": "latest",
"jotai-tanstack-query": "latest",
"jotai-tanstack-query": "../../",
"react": "latest",
"react-dom": "latest"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/02_typescript_suspense/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"@tanstack/query-core": "latest",
"jotai": "latest",
"jotai-tanstack-query": "latest",
"jotai-tanstack-query": "../../",
"react": "latest",
"react-dom": "latest"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/03_infinite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"@tanstack/query-core": "latest",
"jotai": "latest",
"jotai-tanstack-query": "latest",
"jotai-tanstack-query": "../../",
"react": "latest",
"react-dom": "latest"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/04_infinite_suspense/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"@tanstack/query-core": "latest",
"jotai": "latest",
"jotai-tanstack-query": "latest",
"jotai-tanstack-query": "../../",
"react": "latest",
"react-dom": "latest"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/05_mutation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"@tanstack/query-core": "latest",
"jotai": "latest",
"jotai-tanstack-query": "latest",
"jotai-tanstack-query": "../../",
"react": "latest",
"react-dom": "latest"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/06_refetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"@tanstack/query-core": "latest",
"jotai": "latest",
"jotai-tanstack-query": "latest",
"jotai-tanstack-query": "../../",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the fix

"react": "latest",
"react-dom": "latest"
},
Expand Down
12 changes: 12 additions & 0 deletions examples/07_queries/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!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>
24 changes: 24 additions & 0 deletions examples/07_queries/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "jotai-tanstack-query-example",
"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"
}
}
150 changes: 150 additions & 0 deletions examples/07_queries/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React from 'react'
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 UsersData = () => {
const [userIds] = useAtom(userIdsAtom)

const userQueryAtoms = atomWithQueries<User>({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

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>
<h3>Users: </h3>
<div>
{userQueryAtoms.map((queryAtom, index) => (
<Data key={index} queryAtom={queryAtom} />
))}
</div>
</div>
)
}

const CombinedUsersData = () => {
const [userIds] = useAtom(userIdsAtom)

const combinedUsersDataAtom = 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),
}
},
})

return (
<div>
<h3>Users: (combinedQueries)</h3>
<div>
<CombinedData queryAtom={combinedUsersDataAtom} />
</div>
</div>
)
}

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

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

return (
<div>
{data.map((user) => (
<UserDisplay key={user.id} user={user} />
))}
</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>

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={() =>
setUserIds(
Array.from({ length: 3 }, () => Math.floor(Math.random() * 10) + 1)
)
}>
Random
</button>
</div>
)
}

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

export default App
8 changes: 8 additions & 0 deletions examples/07_queries/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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))
}
25 changes: 25 additions & 0 deletions examples/07_queries/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"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" }]
}
Loading