Skip to content

Commit

Permalink
feat(query): new implementation with peer dep (#1435)
Browse files Browse the repository at this point in the history
* wip(query): new implementation with dep

* pending on action

* pending atom for refetch action

* tweak and skip some tests

* wip: refetch force

* add strict mode

* no force option

* always staleTime

* fix initial data

* update dep

* infinite query

* fix type & skip test

* update dep

* update dep

* update dep

* update dep

* update dep

* experimenting a slightly different impl

* hmmm, skip them again

* seems to work in normal mode

* staleTime was fine. it is just timing thing

* update dep

* update dep

* update dep

* update dep

* update docs

* update dep

* update jotai-tanstack-query v0.4.0
  • Loading branch information
dai-shi authored Oct 26, 2022
1 parent 79fa817 commit 3b0d7cf
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 256 deletions.
107 changes: 85 additions & 22 deletions docs/integrations/query.mdx
Original file line number Diff line number Diff line change
@@ -1,37 +1,58 @@
---
title: Query
description: This doc describes `jotai/query` bundle.
title: TanStack Query
description: This doc describes TanStack Query integration.
nav: 4.03
---

This is the Jotai integration with [TanStack Query v4](https://tanstack.com/query/v4), a set of hooks and tools for managing async state (typically external data).
[TanStack Query](https://tanstack.com/query/) provides a set of functions for managing async state (typically external data).

From the [Overview docs](https://tanstack.com/query/v4/docs/overview):

> 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 with TanStack Query 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](https://github.com/jotai-labs/jotai-tanstack-query) is a Jotai integration 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 `@tanstack/query-core` to make use of this bundle and its functions.
In addition to `jotai`, you have to install `jotai-tanstack-query` and `@tanstack/query-core` to use the integration.

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

## atomWithQuery
## Exported functions

`atomWithQuery` creates a new atom that implements a standard [`Query`](https://tanstack.com/query/v4/docs/guides/queries) from TanStack Query. This function combines both Jotai `atom` features and `useQuery` features in a single atom. This atom supports all features you'd expect to find when using the standard `@tanstack/react-query` package.
- `atomsWithQuery` for [QueryObserver](https://tanstack.com/query/v4/docs/reference/QueryObserver)
- `atomsWithInfiniteQuery` for [InfiniteQueryObserver](https://tanstack.com/query/v4/docs/reference/InfiniteQueryObserver)
- `atomsWithMutation` for MutationObserver

All three functions follow the same signature.

```ts
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](https://tanstack.com/query/v4/docs/reference/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 Suspnse.
The data from the observer is also included in `statusAtom`,
so if you don't use Suspense, you don't need to use `dataAtom`.

## `atomsWithQuery` usage

`atomsWithQuery` creates new atoms that implement a standard [`Query`](https://tanstack.com/query/v4/docs/guides/queries) 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.
```jsx
import { useAtom } from 'jotai'
import { atomWithQuery } from 'jotai/query'
import { atomsWithQuery } from 'jotai-tanstack-query'

const idAtom = atom(1)
const userAtom = atomWithQuery((get) => ({
const [userAtom] = atomsWithQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
Expand All @@ -45,20 +66,20 @@ const UserData = () => {
}
```

## atomWithInfiniteQuery
## `atomsWithTansackInfiniteQuery` usage

`atomWithInfiniteQuery` is very similar to `atomWithQuery`, however it creates 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/v4/docs/guides/infinite-queries). This atom supports all features you'd expect to find when using the standard `@tanstack/react-query` package.
`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](https://tanstack.com/query/v4/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 { useAtom } from 'jotai'
import { atomWithInfiniteQuery } from 'jotai/query'
import { atomsWithInfiniteQuery } from 'jotai-tanstack-query'

const idAtom = atom(1)
const userAtom = atomWithInfiniteQuery((get) => ({
const [userAtom] = atomsWithInfiniteQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
Expand All @@ -78,17 +99,50 @@ const UserData = () => {

### Fetching pages and refetching

Using the same atom as in the above example, we can use the `update` method returned from `useAtom` to call different `@tanstack/react-query` methods:
Using the same atom as in the above example, we can dispatch an action to `userAtom`.

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

const handleFetchNextPage = () => update({ type: 'fetchNextPage' })
## `atomsWithTansackMutation` usage

const handleFetchPreviousPage = () => update({ type: 'fetchPreviousPage' })
`atomsWithMutation` creates new atoms that implement a standard [`Mutatioin`](https://tanstack.com/query/v4/docs/guides/mutations) from TanStack Query.

const handleRefetchAllPages = () => update({ type: 'refetch' })
> Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects.
```jsx
import { 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>
)
}
```

Expand All @@ -100,6 +154,8 @@ To ensure that you reference the same `QueryClient` object, be sure to wrap the

Without this step, `useQueryAtom` will reference a seperate `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`.
Expand All @@ -117,7 +173,7 @@ import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { atomWithQuery, queryClientAtom } from 'jotai/query'
import { atomsWithQuery, queryClientAtom } from 'jotai-tanstack-query'

const queryClient = new QueryClient()
export const App = () => {
Expand All @@ -133,7 +189,7 @@ export const App = () => {
)
}

export const todosAtom = atomWithQuery((get) => {
export const [todosAtom] = atomsWithQuery((get) => {
return {
queryKey: ['todos'],
queryFn: () => fetch('/todos'),
Expand All @@ -159,21 +215,28 @@ export const useTodoMutation = () => {

## SSR support

Both 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/v4/docs/guides/ssr) that React Query supports for use within SSR apps, [hydration](https://tanstack.com/query/v4/docs/guides/ssr#using-hydration) or [`initialData`](https://tanstack.com/query/v4/docs/guides/ssr#using-initialdata).
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/v4/docs/guides/ssr) that React Query supports for use within SSR apps, [hydration](https://tanstack.com/query/v4/docs/guides/ssr#using-hydration) or [`initialData`](https://tanstack.com/query/v4/docs/guides/ssr#using-initialdata).

## Error handling

With `dataAtom`,
Fetch error will be thrown and can be caught with ErrorBoundary.
Refetching may recover from a temporary error.

FIXME: Update example

See [a working example](https://codesandbox.io/s/04mepx) to learn more.

## Examples

### Basic demo

FIXME: Update demo

<CodeSandbox id="ij2sd" />

### Hackernews

FIXME: Update demo

<CodeSandbox id="u4sli" />
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@
"immer": "^9.0.15",
"jest": "^29.2.0",
"jest-environment-jsdom": "^29.2.0",
"jotai-tanstack-query": "^0.4.0",
"json": "^11.0.0",
"optics-ts": "^2.3.0",
"postinstall-postinstall": "^2.1.0",
Expand Down Expand Up @@ -265,9 +266,9 @@
"peerDependencies": {
"@babel/core": "*",
"@babel/template": "*",
"@tanstack/query-core": "*",
"@urql/core": "*",
"immer": "*",
"jotai-tanstack-query": "*",
"optics-ts": "*",
"react": ">=16.8",
"valtio": "*",
Expand All @@ -287,10 +288,10 @@
"immer": {
"optional": true
},
"optics-ts": {
"jotai-tanstack-query": {
"optional": true
},
"@tanstack/query-core": {
"optics-ts": {
"optional": true
},
"valtio": {
Expand Down
127 changes: 21 additions & 106 deletions src/query/atomWithInfiniteQuery.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { InfiniteQueryObserver, isCancelledError } from '@tanstack/query-core'
import type {
InfiniteData,
InfiniteQueryObserverOptions,
QueryKey,
QueryObserverResult,
RefetchOptions,
RefetchQueryFilters,
} from '@tanstack/query-core'
import { atomsWithInfiniteQuery } from 'jotai-tanstack-query'
import { atom } from 'jotai'
import type { WritableAtom } from 'jotai'
import type { Getter, WritableAtom } from 'jotai'
import { queryClientAtom } from './queryClientAtom'
import { CreateQueryOptions, GetQueryClient } from './types'

Expand Down Expand Up @@ -117,113 +116,29 @@ export function atomWithInfiniteQuery<
InfiniteData<TData | TQueryData> | undefined,
AtomWithInfiniteQueryAction<TQueryFnData>
> {
type Result = QueryObserverResult<InfiniteData<TData>, TError>
type State = {
isMounted: boolean
unsubscribe: (() => void) | null
}
const queryDataAtom = atom(
const getOptions = (get: Getter) => ({
staleTime: 200,
...(typeof createQuery === 'function' ? createQuery(get) : createQuery),
})
const [dataAtom] = atomsWithInfiniteQuery(getOptions, getQueryClient)
return atom(
(get) => {
const queryClient = getQueryClient(get)
const options =
typeof createQuery === 'function' ? createQuery(get) : createQuery

const defaultedOptions = queryClient.defaultQueryOptions(options)
const observer = new InfiniteQueryObserver(queryClient, defaultedOptions)
const initialResult = observer.getCurrentResult()

let resolve: ((result: Result) => void) | null = null
const resultAtom = atom<Result | Promise<Result>>(
initialResult.data === undefined && options.enabled !== false
? new Promise<Result>((r) => {
resolve = r
})
: initialResult
)
let setResult: (result: Result) => void = () => {
throw new Error('setting result without mount')
}
const state: State = {
isMounted: false,
unsubscribe: null,
}
const listener = (result: Result) => {
if (
result.isFetching ||
(!result.isError && result.data === undefined) ||
(result.isError && isCancelledError(result.error))
) {
return
}
if (resolve) {
setTimeout(() => {
if (!state.isMounted) {
state.unsubscribe?.()
state.unsubscribe = null
}
}, 1000)
resolve(result)
resolve = null
} else {
setResult(result)
}
}
if (options.enabled !== false) {
state.unsubscribe = observer.subscribe(listener)
}
resultAtom.onMount = (update) => {
setResult = update
state.isMounted = true
if (options.enabled !== false && !state.unsubscribe) {
state.unsubscribe = observer.subscribe(listener)
listener(observer.getCurrentResult())
}
return () => state.unsubscribe?.()
}
return { options, resultAtom, observer, state }
},
(get, set, action: AtomWithInfiniteQueryAction<TQueryFnData>) => {
const { options, resultAtom, observer, state } = get(queryDataAtom)
const options = getOptions(get)
if (options.enabled === false) {
return
const queryClient = getQueryClient(get)
return queryClient.getQueryData<InfiniteData<TData>>(options.queryKey)
}
switch (action.type) {
case 'refetch': {
set(resultAtom, new Promise<never>(() => {})) // infinite pending
if (!state.isMounted) {
state.unsubscribe?.()
state.unsubscribe = null
}
observer.refetch(action.payload).then((result) => {
set(resultAtom, result)
})
return
}
case 'fetchPreviousPage': {
observer.fetchPreviousPage()
return
}
case 'fetchNextPage': {
observer.fetchNextPage()
return
}
return get(dataAtom)
},
(_get, set, action: AtomWithInfiniteQueryAction<TQueryFnData>) => {
if (action.type === 'refetch') {
return set(dataAtom, {
type: 'refetch',
force: true,
options: action.payload as any,
})
}
return set(dataAtom, action)
}
)

const queryAtom = atom<
InfiniteData<TData> | undefined,
AtomWithInfiniteQueryAction<TQueryFnData>
>(
(get) => {
const { resultAtom } = get(queryDataAtom)
const result = get(resultAtom)
if (result.isError) {
throw result.error
}
return result.data
},
(_get, set, action) => set(queryDataAtom, action) // delegate action
)
return queryAtom
}
Loading

1 comment on commit 3b0d7cf

@vercel
Copy link

@vercel vercel bot commented on 3b0d7cf Oct 26, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.