diff --git a/src/utils/loadable.ts b/src/utils/loadable.ts index 14b77d32c9..9539816abd 100644 --- a/src/utils/loadable.ts +++ b/src/utils/loadable.ts @@ -2,12 +2,14 @@ import { atom } from 'jotai' import type { Atom } from 'jotai' import { createMemoizeAtom } from './weakCache' +type ResolveType = T extends Promise ? V : T + const memoizeAtom = createMemoizeAtom() type Loadable = | { state: 'loading' } | { state: 'hasError'; error: unknown } - | { state: 'hasData'; data: Value } + | { state: 'hasData'; data: ResolveType } export function loadable(anAtom: Atom): Atom> { return memoizeAtom(() => { @@ -18,7 +20,7 @@ export function loadable(anAtom: Atom): Atom> { const ref = get(refAtom) let curr = ref.prev try { - const value = get(anAtom) + const value = get(anAtom) as ResolveType if (curr?.state !== 'hasData' || !Object.is(curr.data, value)) { curr = { state: 'hasData', data: value } } diff --git a/src/utils/selectAtom.ts b/src/utils/selectAtom.ts index 8b0acc1ccc..4c46ab3a21 100644 --- a/src/utils/selectAtom.ts +++ b/src/utils/selectAtom.ts @@ -2,18 +2,20 @@ import { atom } from 'jotai' import type { Atom } from 'jotai' import { createMemoizeAtom } from './weakCache' +type ResolveType = T extends Promise ? V : T + const memoizeAtom = createMemoizeAtom() export function selectAtom( anAtom: Atom, - selector: (v: Value) => Slice, + selector: (v: ResolveType) => Slice, equalityFn: (a: Slice, b: Slice) => boolean = Object.is ): Atom { return memoizeAtom(() => { // TODO we should revisit this for a better solution than refAtom const refAtom = atom(() => ({} as { prev?: Slice })) const derivedAtom = atom((get) => { - const slice = selector(get(anAtom)) + const slice = selector(get(anAtom) as ResolveType) const ref = get(refAtom) if ('prev' in ref && equalityFn(ref.prev as Slice, slice)) { return ref.prev as Slice diff --git a/tests/utils/loadable.test.tsx b/tests/utils/loadable.test.tsx index 0480b4310f..df5e456106 100644 --- a/tests/utils/loadable.test.tsx +++ b/tests/utils/loadable.test.tsx @@ -145,5 +145,8 @@ const LoadableComponent = ({ asyncAtom }: LoadableComponentProps) => { return <>{String(value.error)} } - return <>Data: {value.data} + // this is to ensure correct typing + const data: number | string = value.data + + return <>Data: {data} } diff --git a/tests/utils/selectAtom.test.tsx b/tests/utils/selectAtom.test.tsx index 9dcb368ae8..01e3c76e85 100644 --- a/tests/utils/selectAtom.test.tsx +++ b/tests/utils/selectAtom.test.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react' +import { Suspense, useEffect, useRef } from 'react' import { fireEvent, render } from '@testing-library/react' import { atom } from 'jotai' import { selectAtom, useAtomValue, useUpdateAtom } from 'jotai/utils' @@ -58,6 +58,53 @@ it('selectAtom works as expected', async () => { await findByText('a: 3') }) +it('selectAtom works with async atom', async () => { + const bigAtom = atom({ a: 0, b: 'othervalue' }) + const bigAtomAsync = atom((get) => Promise.resolve(get(bigAtom))) + const littleAtom = selectAtom(bigAtomAsync, (v) => v.a) + + const Parent = () => { + const setValue = useUpdateAtom(bigAtom) + return ( + <> + + + ) + } + + const Selector = () => { + const a = useAtomValue(littleAtom) + return ( + <> +
a: {a}
+ + ) + } + + const { findByText, getByText } = render( + + + + + + + ) + + await findByText('a: 0') + + fireEvent.click(getByText('increment')) + await findByText('a: 1') + fireEvent.click(getByText('increment')) + await findByText('a: 2') + fireEvent.click(getByText('increment')) + await findByText('a: 3') +}) + it('do not update unless equality function says value has changed', async () => { const bigAtom = atom({ a: 0 }) const littleAtom = selectAtom(