Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: pmndrs/jotai
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: d1c6d0328ffc5357ecf6bcc714bc959eba14c4eb
Choose a base ref
..
head repository: pmndrs/jotai
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 346d96683417b59c0ec73329ea93acbae843d448
Choose a head ref
Showing with 4 additions and 175 deletions.
  1. +4 −18 src/vanilla/utils/selectAtom.ts
  2. +0 −140 tests/react/vanilla-utils/selectAtom.test.tsx
  3. +0 −17 tests/vanilla/utils/types.test.tsx
22 changes: 4 additions & 18 deletions src/vanilla/utils/selectAtom.ts
Original file line number Diff line number Diff line change
@@ -15,46 +15,32 @@ const memo3 = <T>(
return getCached(create, cache3, dep3)
}

type PromiseOrValue<T> = T | Promise<T>

export function selectAtom<Value, Slice>(
anAtom: Atom<Value>,
selector: (v: Value, prevSlice?: Slice) => Slice,
equalityFn?: (a: Slice, b: Slice) => boolean,
): Atom<Slice>

export function selectAtom<Value, Slice extends Promise<unknown>>(
anAtom: Atom<Value>,
selector: (v: Value, prevSlice?: Slice) => Slice,
equalityFn?: (a: Slice, b: Slice) => Promise<boolean>,
): Atom<Promise<Slice>>

export function selectAtom<Value, Slice>(
anAtom: Atom<Value>,
selector: (v: Value, prevSlice?: PromiseOrValue<Slice>) => Slice,
equalityFn: (
prevSlice: PromiseOrValue<Slice>,
slice: PromiseOrValue<Slice>,
) => PromiseOrValue<boolean> = Object.is,
selector: (v: Value, prevSlice?: Slice) => Slice,
equalityFn: (prevSlice: Slice, slice: Slice) => boolean = Object.is,
) {
return memo3(
() => {
const EMPTY = Symbol()
const selectValue = ([value, prevSlice]: readonly [
Value,
PromiseOrValue<Slice> | typeof EMPTY,
Slice | typeof EMPTY,
]) => {
if (prevSlice === EMPTY) {
return selector(value)
}
const slice = selector(value, prevSlice)
const areEqual = equalityFn(prevSlice, slice)
if (areEqual instanceof Promise) {
return areEqual.then((areEqual) => (areEqual ? prevSlice : slice))
}
return areEqual ? prevSlice : slice
}
const derivedAtom: Atom<PromiseOrValue<Slice> | typeof EMPTY> & {
const derivedAtom: Atom<Slice | typeof EMPTY> & {
init?: typeof EMPTY
} = atom((get) => {
const prev = get(derivedAtom)
140 changes: 0 additions & 140 deletions tests/react/vanilla-utils/selectAtom.test.tsx
Original file line number Diff line number Diff line change
@@ -58,54 +58,6 @@ 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, async (v) => (await v).a)

const Parent = () => {
const setValue = useSetAtom(bigAtom)
return (
<>
<button
onClick={() =>
setValue((oldValue) => ({ ...oldValue, a: oldValue.a + 1 }))
}
>
increment
</button>
</>
)
}

const Selector = () => {
const a = useAtomValue(littleAtom)
return (
<>
<div>a: {a}</div>
</>
)
}

const { findByText, getByText } = render(
<StrictMode>
<Suspense fallback={null}>
<Parent />
<Selector />
</Suspense>
</StrictMode>,
)

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(
@@ -177,95 +129,3 @@ it('do not update unless equality function says value has changed', async () =>
await findByText('value: {"a":3}')
await findByText('commits: 4')
})

it('equality function works even if suspend', async () => {
const bigAtom = atom({ a: 0 })
const bigAtomAsync = atom((get) => Promise.resolve(get(bigAtom)))
const littleAtom = selectAtom(
bigAtomAsync,
async (value) => await value,
async (left, right) => (await left).a === (await right).a,
)

const Controls = () => {
const [value, setValue] = useAtom(bigAtom)
return (
<>
<div>bigValue: {JSON.stringify(value)}</div>
<button
onClick={() =>
setValue((oldValue) => ({ ...oldValue, a: oldValue.a + 1 }))
}
>
increment
</button>
<button onClick={() => setValue((oldValue) => ({ ...oldValue, b: 2 }))}>
other
</button>
</>
)
}

const Selector = () => {
const value = useAtomValue(littleAtom)
return <div>littleValue: {JSON.stringify(value)}</div>
}

const { findByText, getByText } = render(
<StrictMode>
<Suspense fallback={null}>
<Controls />
<Selector />
</Suspense>
</StrictMode>,
)

await findByText('bigValue: {"a":0}')
await findByText('littleValue: {"a":0}')

fireEvent.click(getByText('increment'))
await findByText('bigValue: {"a":1}')
await findByText('littleValue: {"a":1}')

fireEvent.click(getByText('other'))
await findByText('bigValue: {"a":1,"b":2}')
await findByText('littleValue: {"a":1}')
})

it('should not return async value when the base atom values are synchronous', async () => {
expect.assertions(4)
type Base = { id: number; value: number }
const initialBase = Promise.resolve({ id: 0, value: 0 })
const baseAtom = atom<Base | Promise<Base>>(initialBase)

const idAtom = selectAtom(
baseAtom,
(base) => {
if (isPromiseLike(base)) {
return base.then((b) => b.id)
}
return base.id
},
(a, b) => {
if (isPromiseLike(b)) {
return Promise.all([a, b]).then(([a, b]) => a === b) as any
}
return a === b
},
)

const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
typeof (x as any)?.then === 'function'

const store = createStore()
async function incrementValue() {
const { id, value } = await store.get(baseAtom)
store.set(baseAtom, { id, value: value + 1 })
}

expect(isPromiseLike(store.get(baseAtom))).toBe(true)
expect(isPromiseLike(store.get(idAtom))).toBe(true)
await incrementValue()
expect(isPromiseLike(store.get(baseAtom))).toBe(false)
expect(isPromiseLike(store.get(idAtom))).toBe(false)
})
17 changes: 0 additions & 17 deletions tests/vanilla/utils/types.test.tsx
Original file line number Diff line number Diff line change
@@ -7,26 +7,9 @@ import { selectAtom, unwrap } from 'jotai/vanilla/utils'

it('selectAtom() should return the correct types', () => {
const doubleCount = (x: number) => x * 2
const asyncDoubleCount = async (x: Promise<number>) => (await x) * 2
const maybeAsyncDoubleCount = (x: number | Promise<number>) => {
return typeof x === 'number' ? doubleCount(x) : asyncDoubleCount(x)
}
const syncAtom = atom(0)
const syncSelectedAtom = selectAtom(syncAtom, doubleCount)
expectType<TypeEqual<Atom<number>, typeof syncSelectedAtom>>(true)

const asyncAtom = atom(Promise.resolve(0))
const asyncSelectedAtom = selectAtom(asyncAtom, asyncDoubleCount)
expectType<TypeEqual<Atom<Promise<number>>, typeof asyncSelectedAtom>>(true)

const maybeAsyncAtom = atom(Promise.resolve(0) as number | Promise<number>)
const maybeAsyncSelectedAtom = selectAtom(
maybeAsyncAtom,
maybeAsyncDoubleCount,
)
expectType<
TypeEqual<Atom<number | Promise<number>>, typeof maybeAsyncSelectedAtom>
>(true)
})

it('unwrap() should return the correct types', () => {