Skip to content

Commit

Permalink
feat: jotai/query (powered by react-query) (pmndrs#248)
Browse files Browse the repository at this point in the history
* wip: jotai/query with react-query

* fix package json

* re implement jotai query

* possible fix pending hack

* fix: observer and pending

* fix: copy script

* chore: smplify code

* chore: fix typo

* query basic test

* refetch query

* refetch query test

* typo

* refactor atomWithQuery

* fix failing test

* chore: refactor

* chore: simplify test

* chore: refactor create pending

* better react suspense

* chore: precise types

* wip: re-implement atomWithQuery

* fix: initializing observe atom

* fix: add optional peer dependency

* update size snapshot

* query loading

* fix: making a test to fail

* update docs

* query loading 2

* new impl

* new impl test

* new typing

* fix csb loading problems

* new typing and a small fix

* rename the type

* reset pending on new fetch

* some minor fixes

* import type only from relative path

* update react-query

Co-authored-by: M. Bagher Abiat <[email protected]>
  • Loading branch information
dai-shi and Aslemammad authored Feb 19, 2021
1 parent 23dbb02 commit df33993
Show file tree
Hide file tree
Showing 10 changed files with 465 additions and 5 deletions.
19 changes: 19 additions & 0 deletions .size-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@
}
}
},
"query.module.js": {
"bundled": 3006,
"minified": 1208,
"gzipped": 579,
"treeshaked": {
"rollup": {
"code": 57,
"import_statements": 49
},
"webpack": {
"code": 1078
}
}
},
"index.js": {
"bundled": 23931,
"minified": 10862,
Expand All @@ -93,5 +107,10 @@
"bundled": 1028,
"minified": 527,
"gzipped": 317
},
"query.js": {
"bundled": 4729,
"minified": 2085,
"gzipped": 914
}
}
38 changes: 38 additions & 0 deletions docs/api/query.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
This doc describes `jotai/query` bundle.

## Install

You have to install `react-query` to access this bundle and its functions.

```
yarn add react-query
```

## atomWithQuery

`atomWithQuery` creates a new atom with React Query. This function helps you use both atoms features and `useQuery` features in a single atom.

```js
import { useAtom } from 'jotai'
import { atomWithQuery } from 'jotai/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] = useAtom(userAtom)
return <div>{JSON.stringify(data)}</div>
}
```

### Examples

Basic demo: [codesandbox](https://codesandbox.io/s/jotai-query-demo-ij2sd)

Hackernews: [codesandbox](https://codesandbox.io/s/jotai-query-hacker-news-u4sli)
4 changes: 1 addition & 3 deletions docs/introduction/showcase.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Showcase

## Official examples

- Text Length example [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square&logo=codesandbox)](https://githubbox.com/pmndrs/jotai/tree/master/examples/text_length)

Count the length and show the uppercase of any text.
Expand All @@ -24,4 +22,4 @@

- Tic Tac Toe game [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square&logo=codesandbox)](https://codesandbox.io/s/jotai-tic-tac-6cg3h)

A game of tic tac toe implemented with jotai.
A game of tic tac toe implemented with jotai.
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
"import": "./optics.module.js",
"require": "./optics.js",
"types": "./optics.d.ts"
},
"./query": {
"import": "./query.module.mjs",
"require": "./query.js",
"types": "./query.d.ts"
}
},
"files": [
Expand All @@ -56,7 +61,7 @@
"test": "jest && jest --setupFiles ./tests/setReactExperimental.ts",
"test:dev": "jest --watch --no-coverage",
"test:coverage:watch": "jest --watch",
"copy": "shx mv dist/src/* dist && shx rm -rf dist/{src,tests} && shx cp dist/index.d.ts dist/index.module.d.ts && shx cp dist/utils.d.ts dist/utils.module.d.ts && shx cp dist/devtools.d.ts dist/devtools.module.d.ts && shx cp dist/immer.d.ts dist/immer.module.d.ts && shx cp dist/optics.d.ts dist/optics.module.d.ts && downlevel-dts dist dist/ts3.4 && shx cp package.json readme.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.husky=undefined; this.prettier=undefined; this.jest=undefined; this['lint-staged']=undefined;\""
"copy": "shx mv dist/src/* dist && shx rm -rf dist/{src,tests} && shx cp dist/index.d.ts dist/index.module.d.ts && shx cp dist/utils.d.ts dist/utils.module.d.ts && shx cp dist/devtools.d.ts dist/devtools.module.d.ts && shx cp dist/immer.d.ts dist/immer.module.d.ts && shx cp dist/optics.d.ts dist/optics.module.d.ts && shx cp dist/query.d.ts dist/query.module.d.ts && downlevel-dts dist dist/ts3.4 && shx cp package.json readme.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.husky=undefined; this.prettier=undefined; this.jest=undefined; this['lint-staged']=undefined;\""
},
"husky": {
"hooks": {
Expand Down Expand Up @@ -152,6 +157,7 @@
"prettier": "^2.2.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-query": "^3.9.8",
"rollup": "^2.39.0",
"rollup-plugin-size-snapshot": "^0.12.0",
"shx": "^0.3.3",
Expand All @@ -163,6 +169,7 @@
"react": ">=16.8",
"react-dom": "*",
"react-native": "*",
"react-query": "*",
"scheduler": ">=0.19"
},
"peerDependenciesMeta": {
Expand All @@ -177,6 +184,9 @@
},
"react-native": {
"optional": true
},
"react-query": {
"optional": true
}
}
}
2 changes: 2 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ export default (args) =>
createCommonJSConfig('src/devtools.ts', 'dist/devtools.js'),
createCommonJSConfig('src/immer.ts', 'dist/immer.js'),
createCommonJSConfig('src/optics.ts', 'dist/optics.js'),
createCommonJSConfig('src/query.ts', 'dist/query.js'),
]
: [
createESMConfig('src/index.ts', 'dist/index.module.js'),
createESMConfig('src/utils.ts', 'dist/utils.module.js'),
createESMConfig('src/devtools.ts', 'dist/devtools.module.js'),
createESMConfig('src/immer.ts', 'dist/immer.module.js'),
createESMConfig('src/optics.ts', 'dist/optics.module.js'),
createESMConfig('src/query.ts', 'dist/query.module.js'),
]
1 change: 1 addition & 0 deletions src/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { atomWithQuery } from './query/atomWithQuery'
151 changes: 151 additions & 0 deletions src/query/atomWithQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {
QueryClient,
QueryKey,
QueryObserver,
QueryObserverOptions,
} from 'react-query'
import { WritableAtom, atom } from 'jotai'
import type { Getter, Setter } from '../core/types'

type ResultActions = { type: 'refetch' }
type AtomQueryOptions<
TQueryFnData,
TError,
TData,
TQueryData
> = QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> & {
queryKey: QueryKey
}

const queryClientAtom = atom<QueryClient | null>(null)
const getQueryClient = (get: Getter, set: Setter): QueryClient => {
let queryClient = get(queryClientAtom)
if (queryClient === null) {
queryClient = new QueryClient()
set(queryClientAtom, queryClient)
}
return queryClient
}

const createPending = <T>() => {
const pending: {
fulfilled: boolean
promise?: Promise<T>
resolve?: (data: T) => void
} = {
fulfilled: false,
}
pending.promise = new Promise<T>((resolve) => {
pending.resolve = (data: T) => {
resolve(data)
pending.fulfilled = true
}
})
return pending as {
fulfilled: boolean
promise: Promise<T>
resolve: (data: T) => void
}
}

export function atomWithQuery<
TQueryFnData,
TError,
TData = TQueryFnData,
TQueryData = TQueryFnData
>(
createQuery:
| AtomQueryOptions<TQueryFnData, TError, TData, TQueryData>
| ((
get: Getter
) => AtomQueryOptions<TQueryFnData, TError, TData, TQueryData>)
): WritableAtom<TData, ResultActions> {
const pendingAtom = atom(createPending<TData>())
const dataAtom = atom<TData | null>(null)
const queryAtom = atom<
[
AtomQueryOptions<TQueryFnData, TError, TData, TQueryData>,
WritableAtom<null, any>
],
ResultActions
>(
(get) => {
const options =
typeof createQuery === 'function' ? createQuery(get) : createQuery
const observerAtom = atom(
null,
(
get,
set,
action:
| { type: 'init'; intializer: (queryClient: QueryClient) => void }
| { type: 'data'; data: TData }
) => {
if (action.type === 'init') {
const pending = get(pendingAtom)
if (pending.fulfilled) {
set(pendingAtom, createPending<TData>()) // new fetch
}
action.intializer(getQueryClient(get, set))
} else if (action.type === 'data') {
set(dataAtom, action.data)
const pending = get(pendingAtom)
if (!pending.fulfilled) {
pending.resolve(action.data)
}
}
}
)
observerAtom.onMount = (dispatch) => {
let unsub: (() => void) | undefined | false
const intializer = (queryClient: QueryClient) => {
const observer = new QueryObserver(queryClient, options)
observer.subscribe((result) => {
// TODO error handling
if (result.data !== undefined) {
dispatch({ type: 'data', data: result.data })
}
})
if (unsub === false) {
observer.destroy()
} else {
unsub = () => {
observer.destroy()
}
}
}
dispatch({ type: 'init', intializer })
return () => {
if (unsub) {
unsub()
}
unsub = false
}
}
return [options, observerAtom]
},
async (get, set, action) => {
if (action.type === 'refetch') {
const [options] = get(queryAtom)
set(pendingAtom, createPending<TData>()) // reset pending
getQueryClient(get, set).getQueryCache().find(options.queryKey)?.reset()
await getQueryClient(get, set).refetchQueries(options.queryKey)
}
}
)
const queryDataAtom = atom<TData, ResultActions>(
(get) => {
const [, observerAtom] = get(queryAtom)
get(observerAtom) // use it here
const data = get(dataAtom)
const pending = get(pendingAtom)
if (!pending.fulfilled) {
return pending.promise
}
// we are sure that data is not null
return data as TData
},
(_get, set, action) => set(queryAtom, action) // delegate action
)
return queryDataAtom
}
Loading

0 comments on commit df33993

Please sign in to comment.