Skip to content

Commit a28dde4

Browse files
author
David Maskasky
committed
feat(atomFamily): support getParams and unstable_listen api
1 parent aeeb479 commit a28dde4

File tree

2 files changed

+131
-3
lines changed

2 files changed

+131
-3
lines changed

src/vanilla/utils/atomFamily.ts

+37-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import type { Atom } from '../../vanilla.ts'
22

33
type ShouldRemove<Param> = (createdAt: number, param: Param) => boolean
4+
type Cleanup = () => void
5+
type Callback<Param, AtomType> = (event: {
6+
type: 'CREATE' | 'REMOVE'
7+
param: Param
8+
atom: AtomType
9+
}) => void
410

511
export interface AtomFamily<Param, AtomType> {
612
(param: Param): AtomType
13+
getParams(): Iterable<Param>
714
remove(param: Param): void
815
setShouldRemove(shouldRemove: ShouldRemove<Param> | null): void
16+
unstable_listen(callback: Callback<Param, AtomType>): Cleanup
917
}
1018

1119
export function atomFamily<Param, AtomType extends Atom<unknown>>(
@@ -20,6 +28,7 @@ export function atomFamily<Param, AtomType extends Atom<unknown>>(
2028
type CreatedAt = number // in milliseconds
2129
let shouldRemove: ShouldRemove<Param> | null = null
2230
const atoms: Map<Param, [AtomType, CreatedAt]> = new Map()
31+
const listeners = new Set<Callback<Param, AtomType>>()
2332
const createAtom = (param: Param) => {
2433
let item: [AtomType, CreatedAt] | undefined
2534
if (areEqual === undefined) {
@@ -43,16 +52,40 @@ export function atomFamily<Param, AtomType extends Atom<unknown>>(
4352
}
4453

4554
const newAtom = initializeAtom(param)
55+
notifyListeners('CREATE', param, newAtom)
4656
atoms.set(param, [newAtom, Date.now()])
4757
return newAtom
4858
}
4959

60+
function notifyListeners(
61+
type: 'CREATE' | 'REMOVE',
62+
param: Param,
63+
atom: AtomType,
64+
) {
65+
for (const listener of listeners) {
66+
listener({ type, param, atom })
67+
}
68+
}
69+
70+
createAtom.unstable_listen = (callback: Callback<Param, AtomType>) => {
71+
listeners.add(callback)
72+
return () => {
73+
listeners.delete(callback)
74+
}
75+
}
76+
77+
createAtom.getParams = () => atoms.keys()
78+
5079
createAtom.remove = (param: Param) => {
5180
if (areEqual === undefined) {
81+
if (!atoms.has(param)) return
82+
const [atom] = atoms.get(param)!
83+
notifyListeners('REMOVE', param, atom)
5284
atoms.delete(param)
5385
} else {
54-
for (const [key] of atoms) {
86+
for (const [key, [atom]] of atoms) {
5587
if (areEqual(key, param)) {
88+
notifyListeners('REMOVE', key, atom)
5689
atoms.delete(key)
5790
break
5891
}
@@ -63,8 +96,9 @@ export function atomFamily<Param, AtomType extends Atom<unknown>>(
6396
createAtom.setShouldRemove = (fn: ShouldRemove<Param> | null) => {
6497
shouldRemove = fn
6598
if (!shouldRemove) return
66-
for (const [key, value] of atoms) {
67-
if (shouldRemove(value[1], key)) {
99+
for (const [key, [atom, createdAt]] of atoms) {
100+
if (shouldRemove(createdAt, key)) {
101+
notifyListeners('REMOVE', key, atom)
68102
atoms.delete(key)
69103
}
70104
}
+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { expect, it, vi } from 'vitest'
2+
import { type Atom, atom, createStore } from 'jotai/vanilla'
3+
import { atomFamily } from 'jotai/vanilla/utils'
4+
5+
it('should create atoms with different params', () => {
6+
const store = createStore()
7+
const aFamily = atomFamily((param: number) => atom(param))
8+
9+
expect(store.get(aFamily(1))).toEqual(1)
10+
expect(store.get(aFamily(2))).toEqual(2)
11+
})
12+
13+
it('should remove atoms', () => {
14+
const store = createStore()
15+
const initializeAtom = vi.fn((param: number) => atom(param))
16+
const aFamily = atomFamily(initializeAtom)
17+
18+
expect(store.get(aFamily(1))).toEqual(1)
19+
expect(store.get(aFamily(2))).toEqual(2)
20+
aFamily.remove(2)
21+
initializeAtom.mockClear()
22+
expect(store.get(aFamily(1))).toEqual(1)
23+
expect(initializeAtom).toHaveBeenCalledTimes(0)
24+
expect(store.get(aFamily(2))).toEqual(2)
25+
expect(initializeAtom).toHaveBeenCalledTimes(1)
26+
})
27+
28+
it('should remove atoms with custom comparator', () => {
29+
const store = createStore()
30+
const initializeAtom = vi.fn((param: number) => atom(param))
31+
const aFamily = atomFamily(initializeAtom, (a, b) => a === b)
32+
33+
expect(store.get(aFamily(1))).toEqual(1)
34+
expect(store.get(aFamily(2))).toEqual(2)
35+
expect(store.get(aFamily(3))).toEqual(3)
36+
aFamily.remove(2)
37+
initializeAtom.mockClear()
38+
expect(store.get(aFamily(1))).toEqual(1)
39+
expect(initializeAtom).toHaveBeenCalledTimes(0)
40+
expect(store.get(aFamily(2))).toEqual(2)
41+
expect(initializeAtom).toHaveBeenCalledTimes(1)
42+
})
43+
44+
it('should remove atoms with custom shouldRemove', () => {
45+
const store = createStore()
46+
const initializeAtom = vi.fn((param: number) => atom(param))
47+
const aFamily = atomFamily<number, Atom<number>>(initializeAtom)
48+
expect(store.get(aFamily(1))).toEqual(1)
49+
expect(store.get(aFamily(2))).toEqual(2)
50+
expect(store.get(aFamily(3))).toEqual(3)
51+
aFamily.setShouldRemove((_createdAt, param) => param % 2 === 0)
52+
initializeAtom.mockClear()
53+
expect(store.get(aFamily(1))).toEqual(1)
54+
expect(initializeAtom).toHaveBeenCalledTimes(0)
55+
expect(store.get(aFamily(2))).toEqual(2)
56+
expect(initializeAtom).toHaveBeenCalledTimes(1)
57+
expect(store.get(aFamily(3))).toEqual(3)
58+
expect(initializeAtom).toHaveBeenCalledTimes(1)
59+
})
60+
61+
it('should notify listeners', () => {
62+
const store = createStore()
63+
const aFamily = atomFamily((param: number) => atom(param))
64+
type AtomEvent = Parameters<Parameters<typeof aFamily.unstable_listen>[0]>[0]
65+
const listener = vi.fn(({ type, param, atom }: AtomEvent) => {})
66+
const unsubscribe = aFamily.unstable_listen(listener)
67+
const atom1 = aFamily(1)
68+
expect(listener).toHaveBeenCalledTimes(1)
69+
const eventCreate = listener.mock.calls[0][0]
70+
expect(eventCreate.type).toEqual('CREATE')
71+
expect(eventCreate.param).toEqual(1)
72+
expect(eventCreate.atom).toEqual(atom1)
73+
listener.mockClear()
74+
aFamily.remove(1)
75+
expect(listener).toHaveBeenCalledTimes(1)
76+
const eventRemove = listener.mock.calls[0][0]
77+
expect(eventRemove.type).toEqual('REMOVE')
78+
expect(eventRemove.param).toEqual(1)
79+
expect(eventRemove.atom).toEqual(atom1)
80+
unsubscribe()
81+
listener.mockClear()
82+
aFamily(2)
83+
expect(listener).toHaveBeenCalledTimes(0)
84+
})
85+
86+
it('should return all params', () => {
87+
const store = createStore()
88+
const aFamily = atomFamily((param: number) => atom(param))
89+
90+
expect(store.get(aFamily(1))).toEqual(1)
91+
expect(store.get(aFamily(2))).toEqual(2)
92+
expect(store.get(aFamily(3))).toEqual(3)
93+
expect(Array.from(aFamily.getParams())).toEqual([1, 2, 3])
94+
})

0 commit comments

Comments
 (0)