diff --git a/.size-snapshot.json b/.size-snapshot.json index d097ad12bd..391102f17c 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -14,9 +14,9 @@ } }, "utils.module.js": { - "bundled": 7958, - "minified": 3528, - "gzipped": 1427, + "bundled": 8382, + "minified": 3699, + "gzipped": 1481, "treeshaked": { "rollup": { "code": 28, @@ -103,9 +103,9 @@ "gzipped": 3467 }, "utils.js": { - "bundled": 11093, - "minified": 5420, - "gzipped": 2040 + "bundled": 11579, + "minified": 5650, + "gzipped": 2098 }, "devtools.js": { "bundled": 2394, diff --git a/docs/api/utils.md b/docs/api/utils.md index 41089eb538..b65dd6cf94 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -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 diff --git a/src/utils.ts b/src/utils.ts index 5fa9297ce3..3b7fc87321 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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' diff --git a/src/utils/atomWithDefault.ts b/src/utils/atomWithDefault.ts new file mode 100644 index 0000000000..b60713c1b0 --- /dev/null +++ b/src/utils/atomWithDefault.ts @@ -0,0 +1,26 @@ +import { atom, PrimitiveAtom } from 'jotai' +import type { Read } from '../core/types' + +export function atomWithDefault( + getDefault: Read +): PrimitiveAtom { + const EMPTY = Symbol() + const overwrittenAtom = atom(EMPTY) + const anAtom: PrimitiveAtom = 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 +} diff --git a/src/utils/atomWithReset.ts b/src/utils/atomWithReset.ts index 9dd70df5f0..f01fba7ef8 100644 --- a/src/utils/atomWithReset.ts +++ b/src/utils/atomWithReset.ts @@ -6,7 +6,7 @@ export const RESET = Symbol() export function atomWithReset(initialValue: Value) { type Update = SetStateAction | typeof RESET - const anAtom: any = atom(initialValue, (get, set, update) => { + const anAtom = atom(initialValue, (get, set, update) => { if (update === RESET) { set(anAtom, initialValue) } else { diff --git a/tests/utils/atomWithDefault.test.tsx b/tests/utils/atomWithDefault.test.tsx new file mode 100644 index 0000000000..06223c3910 --- /dev/null +++ b/tests/utils/atomWithDefault.test.tsx @@ -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 ( + <> +
+ count1: {count1}, count2: {count2} +
+ + + + ) + } + + const { findByText, getByText } = render( + + + + ) + + 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 ( + <> +
+ count1: {count1}, count2: {count2} +
+ + + + ) + } + + const { findByText, getByText } = render( + + + + + + ) + + 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') +})