Skip to content

Commit

Permalink
docs: add toc, tests: add test for issue 39
Browse files Browse the repository at this point in the history
  • Loading branch information
kalijonn committed Dec 11, 2023
1 parent 61a7434 commit 81ed867
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 9 deletions.
100 changes: 93 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,53 @@
# Jotai Query 🚀 👻

Minimal `@tanstack/query` integration for jotai.
[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)
- [Incremental Adoption](#incremental-adoption)
- [Exported Functions](#exported-functions)
- [atomWithQuery](#atomwithquery-usage)
- [atomWithInfiniteQuery](#atomwithinfinitequery-usage)
- [atomWithMutation](#atomwithmutation-usage)
- [atomWithMutationState](#atomwithmutationstate-usage)
- [Suspense](#suspense)
- [atomWithSuspenseQuery](#atomwithsuspensequery-usage)
- [atomWithSuspenseInfiniteQuery](#atomwithsuspenseinfinitequery-usage)
- [QueryClient Instance](#referencing-the-same-instance-of-query-client-in-your-project)
- [SSR Support](#ssr-support)
- [Error Handling](#error-handling)
- [Dev Tools](#devtools)
- [Migrate to v0.8.0](#migrate-to-v080)

### Support

jotai-tanstack-query currently supports TanStack Query v5.

### Install

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

```bash
yarn add jotai-tanstack-query @tanstack/query-core
yarn add jotai-tanstack-query @tanstack/query-core wonka
```

### 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. [QueryClient Setup](#referencing-the-same-instance-of-query-client-in-your-project).

```jsx
# existing useQueryHook
const { data, isLoading, isError } = useQuery('todos', () => fetch('/todos'));

# jotai-tanstack-query
const todosAtom = atomWithQuery(() => ({
queryKey: ['todos'],
}))

const [{ data, isLoading, isError }] = useAtom(todosAtom)

```

### Exported functions
Expand All @@ -26,7 +62,7 @@ yarn add jotai-tanstack-query @tanstack/query-core
All functions follow the same signature.

```ts
const dataAtom = atomsWithSomething(getOptions, getQueryClient)
const dataAtom = atomWithSomething(getOptions, getQueryClient)
```

The first `getOptions` parameter is a function that returns an input to the observer.
Expand All @@ -41,7 +77,7 @@ import { atom, useAtom } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'

const idAtom = atom(1)
const userAtom = atomsWithQuery((get) => ({
const userAtom = atomWithQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
Expand Down Expand Up @@ -109,7 +145,7 @@ const Posts = () => {

> Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects.
```jsx
```tsx
const postAtom = atomWithMutation(() => ({
mutationKey: ['posts'],
mutationFn: async ({ title }: { title: string }) => {
Expand Down Expand Up @@ -301,7 +337,7 @@ All atoms can be used within the context of a server side rendered app, such as
Fetch error will be thrown and can be caught with ErrorBoundary.
Refetching may recover from a temporary error.

See [a working example](https://codesandbox.io/s/joer59) to learn more.
See [a working example](https://codesandbox.io/s/4gfp6z) to learn more.

### Devtools

Expand Down Expand Up @@ -352,3 +388,53 @@ export const App = () => {
)
}
```

## 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 `isLoading`, `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, isLoading, 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

```
73 changes: 73 additions & 0 deletions __tests__/atomWithQuery_spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -701,3 +701,76 @@ it('renews the result when the query changes and a non stale cache is available'
await expect(() => findByText('loading')).rejects.toThrow()
await findByText('count: 2')
})

// https://github.com/jotaijs/jotai-tanstack-query/pull/40
it(`ensure that setQueryData for an inactive query updates its atom state`, async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnMount: false,
},
},
})

const extraKey = 'uniqueKey'
const pageAtom = atom(1)

const queryFn = jest.fn(() => {
return Promise.resolve('John Doe')
})

const userAtom = atomWithQuery(
() => {
return {
queryKey: [extraKey],
queryFn: async () => {
const name = await queryFn()
return { response: { name } }
},
}
},
() => queryClient
)

const User = () => {
const [{ data, isPending }] = useAtom(userAtom)

if (isPending) return <>loading</>

return <>Name: {data?.response.name}</>
}

const Controls = () => {
const [, setPage] = useAtom(pageAtom)
return (
<>
<button onClick={() => setPage(1)}>Set page 1</button>
<button onClick={() => setPage(2)}>Set page 2</button>
</>
)
}

const App = () => {
const [page] = useAtom(pageAtom)
return (
<>
{page === 1 && <User />}
<Controls />
</>
)
}

const { findByText } = render(
<StrictMode>
<App />
</StrictMode>
)

await findByText('loading')
await findByText('Name: John Doe')
fireEvent.click(await findByText('Set page 2'))
queryClient.setQueryData([extraKey], { response: { name: 'Alex Smith' } })
fireEvent.click(await findByText('Set page 1'))
await expect(() => findByText('loading')).rejects.toThrow()
await findByText('Name: Alex Smith')
})
81 changes: 79 additions & 2 deletions __tests__/atomWithSuspenseQuery_spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ it('suspense basic, suspends', async () => {

const { findByText } = render(
<StrictMode>
<Suspense fallback="loading">
<Suspense fallback="Loading...">
<Counter />
</Suspense>
</StrictMode>
)

await findByText('loading')
await findByText('Loading...')
resolve()
await findByText('count: 0')
})
Expand Down Expand Up @@ -459,3 +459,80 @@ it('on reset, throws suspense', async () => {
resolve()
await findByText('count: 2')
})

// https://github.com/jotaijs/jotai-tanstack-query/pull/40
it(`ensure that setQueryData for an inactive query updates its atom state`, async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnMount: false,
},
},
})

const extraKey = 'uniqueKey'
const pageAtom = atom(1)

const queryFn = jest.fn(() => {
return Promise.resolve('John Doe')
})

const userAtom = atomWithSuspenseQuery(
() => {
return {
queryKey: [extraKey],
queryFn: async () => {
const name = await queryFn()
return { response: { name } }
},
}
},
() => queryClient
)

const User = () => {
const [
{
data: {
response: { name },
},
},
] = useAtom(userAtom)

return <>Name: {name}</>
}

const Controls = () => {
const [, setPage] = useAtom(pageAtom)
return (
<>
<button onClick={() => setPage(1)}>Set page 1</button>
<button onClick={() => setPage(2)}>Set page 2</button>
</>
)
}

const App = () => {
const [page] = useAtom(pageAtom)
return (
<>
<Suspense fallback="loading">{page === 1 && <User />}</Suspense>
<Controls />
</>
)
}

const { findByText } = render(
<StrictMode>
<App />
</StrictMode>
)

await findByText('loading')
await findByText('Name: John Doe')
fireEvent.click(await findByText('Set page 2'))
queryClient.setQueryData([extraKey], { response: { name: 'Alex Smith' } })
fireEvent.click(await findByText('Set page 1'))
await expect(() => findByText('loading')).rejects.toThrow()
await findByText('Name: Alex Smith')
})

0 comments on commit 81ed867

Please sign in to comment.