Skip to content

Commit

Permalink
feat(util): atomWithDefault (#379)
Browse files Browse the repository at this point in the history
* wip: atom with default util

* chore: add tests (failing 😢 )

* atom with default with two atoms

* add atomWithDefault docs

* Add write to atomWithDefault

* Update size snapshot

* Revert "Add write to atomWithDefault"

This reverts commit 3320848.

* refactor in more idiomatic way without relying on undocumented behavior

Co-authored-by: Thisen <[email protected]>
  • Loading branch information
dai-shi and Thisen authored Mar 27, 2021
1 parent c828979 commit 42db545
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 7 deletions.
12 changes: 6 additions & 6 deletions .size-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
}
},
"utils.module.js": {
"bundled": 7958,
"minified": 3528,
"gzipped": 1427,
"bundled": 8382,
"minified": 3699,
"gzipped": 1481,
"treeshaked": {
"rollup": {
"code": 28,
Expand Down Expand Up @@ -103,9 +103,9 @@
"gzipped": 3483
},
"utils.js": {
"bundled": 11093,
"minified": 5420,
"gzipped": 2040
"bundled": 11579,
"minified": 5650,
"gzipped": 2098
},
"devtools.js": {
"bundled": 2394,
Expand Down
16 changes: 16 additions & 0 deletions docs/api/utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,22 @@ const countReducerAtom = atomWithReducer(0, countReducer)

https://codesandbox.io/s/react-typescript-forked-g3tsx

## atomWithDefault

Ref: https://github.com/pmndrs/jotai/issues/352

This is a function to create a primitive atom.
Its default value can be specified with a read function instead of a static initial value.

```js
import { atomWithDefault } from 'jotai/utils'
const count1Atom = atom(1)
const count2Atom = atomWithDefault((get) => get(count1Atom) * 2)
```

https://codesandbox.io/s/react-typescript-forked-unfro

## atomFamily

Ref: https://github.com/pmndrs/jotai/issues/23
Expand Down
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { selectAtom } from './utils/selectAtom'
export { useAtomCallback } from './utils/useAtomCallback'
export { freezeAtom, atomFrozenInDev } from './utils/freezeAtom'
export { splitAtom } from './utils/splitAtom'
export { atomWithDefault } from './utils/atomWithDefault'
26 changes: 26 additions & 0 deletions src/utils/atomWithDefault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { atom, PrimitiveAtom } from 'jotai'
import type { Read } from '../core/types'

export function atomWithDefault<Value>(
getDefault: Read<Value>
): PrimitiveAtom<Value> {
const EMPTY = Symbol()
const overwrittenAtom = atom<Value | typeof EMPTY>(EMPTY)
const anAtom: PrimitiveAtom<Value> = atom(
(get) => {
const overwritten = get(overwrittenAtom)
if (overwritten !== EMPTY) {
return overwritten
}
return getDefault(get)
},
(get, set, update) =>
set(
overwrittenAtom,
typeof update === 'function'
? (update as (prev: Value) => Value)(get(anAtom))
: update
)
)
return anAtom
}
2 changes: 1 addition & 1 deletion src/utils/atomWithReset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const RESET = Symbol()

export function atomWithReset<Value>(initialValue: Value) {
type Update = SetStateAction<Value> | typeof RESET
const anAtom: any = atom<Value, Update>(initialValue, (get, set, update) => {
const anAtom = atom<Value, Update>(initialValue, (get, set, update) => {
if (update === RESET) {
set(anAtom, initialValue)
} else {
Expand Down
85 changes: 85 additions & 0 deletions tests/utils/atomWithDefault.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { Fragment, Suspense } from 'react'
import { fireEvent, render } from '@testing-library/react'
import { Provider as ProviderOrig, atom, useAtom } from '../../src/index'
import { atomWithDefault } from '../../src/utils'

const Provider = process.env.PROVIDER_LESS_MODE ? Fragment : ProviderOrig

it('simple sync get default', async () => {
const count1Atom = atom(1)
const count2Atom = atomWithDefault((get) => get(count1Atom) * 2)

const Counter: React.FC = () => {
const [count1, setCount1] = useAtom(count1Atom)
const [count2, setCount2] = useAtom(count2Atom)
return (
<>
<div>
count1: {count1}, count2: {count2}
</div>
<button onClick={() => setCount1((c) => c + 1)}>button1</button>
<button onClick={() => setCount2((c) => c + 1)}>button2</button>
</>
)
}

const { findByText, getByText } = render(
<Provider>
<Counter />
</Provider>
)

await findByText('count1: 1, count2: 2')

fireEvent.click(getByText('button1'))
await findByText('count1: 2, count2: 4')

fireEvent.click(getByText('button2'))
await findByText('count1: 2, count2: 5')

fireEvent.click(getByText('button1'))
await findByText('count1: 3, count2: 5')
})

it('simple async get default', async () => {
const count1Atom = atom(1)
const count2Atom = atomWithDefault(async (get) => {
await new Promise((r) => setTimeout(r, 10))
return get(count1Atom) * 2
})

const Counter: React.FC = () => {
const [count1, setCount1] = useAtom(count1Atom)
const [count2, setCount2] = useAtom(count2Atom)
return (
<>
<div>
count1: {count1}, count2: {count2}
</div>
<button onClick={() => setCount1((c) => c + 1)}>button1</button>
<button onClick={() => setCount2((c) => c + 1)}>button2</button>
</>
)
}

const { findByText, getByText } = render(
<Provider>
<Suspense fallback="loading">
<Counter />
</Suspense>
</Provider>
)

await findByText('loading')
await findByText('count1: 1, count2: 2')

fireEvent.click(getByText('button1'))
await findByText('loading')
await findByText('count1: 2, count2: 4')

fireEvent.click(getByText('button2'))
await findByText('count1: 2, count2: 5')

fireEvent.click(getByText('button1'))
await findByText('count1: 3, count2: 5')
})

0 comments on commit 42db545

Please sign in to comment.