Skip to content

Commit c81246e

Browse files
iwoplazadai-shi
andauthored
feat(utils): atomWithLazy for lazily initialized primitive atoms. (#2465)
* feat(utils): atomWithLazy for lazily initialized primitive atoms. * tweak test for older ts versions * add `unstable_is` hack for use in `jotai-store` * add documentation for atomWithLazy * update lazy docs * remove semicolons in lazy docs * add more elegant implementation for atomWithLazy * clear type declarations --------- Co-authored-by: Daishi Kato <[email protected]>
1 parent bf075ef commit c81246e

File tree

4 files changed

+150
-0
lines changed

4 files changed

+150
-0
lines changed

docs/utilities/lazy.mdx

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
title: Lazy
3+
nav: 3.03
4+
keywords: lazy,initialize,init,loading
5+
---
6+
7+
When defining primitive atoms, their initial value has to be bound at definition time.
8+
If creating that initial value is computationally expensive, or the value is not accessible during definition,
9+
it would be best to postpone the atom's initialization until its [first use in the store](#using-multiple-stores).
10+
11+
```jsx
12+
const imageDataAtom = atom(initializeExpensiveImage()) // 1) has to be computed here
13+
14+
function Home() {
15+
...
16+
}
17+
18+
function ImageEditor() {
19+
// 2) used only in this route
20+
const [imageData, setImageData] = useAtom(imageDataAtom);
21+
...
22+
}
23+
24+
function App() {
25+
return (
26+
<Router>
27+
<Route path="/" component={Home} />
28+
<Route path="/edit" component={ImageEditor} />
29+
</Router>
30+
)
31+
}
32+
```
33+
34+
## atomWithLazy
35+
36+
Ref: https://github.com/pmndrs/jotai/pull/2465
37+
38+
We can use `atomWithLazy` to create a primitive atom whose initial value will be computed at [first use in the store](#using-multiple-stores).
39+
After initialization, it will behave like a regular primitive atom (can be written to).
40+
41+
### Usage
42+
43+
```jsx
44+
import { atomWithLazy } from 'jotai/utils'
45+
46+
// passing the initialization function
47+
const imageDataAtom = atomWithLazy(initializeExpensiveImage)
48+
49+
function Home() {
50+
...
51+
}
52+
53+
function ImageEditor() {
54+
// only gets initialized if user goes to `/edit`.
55+
const [imageData, setImageData] = useAtom(imageDataAtom);
56+
...
57+
}
58+
59+
function App() {
60+
return (
61+
<Router>
62+
<Route path="/" component={Home} />
63+
<Route path="/edit" component={ImageEditor} />
64+
</Router>
65+
)
66+
}
67+
```
68+
69+
### Using multiple stores
70+
71+
Since each store is its separate universe, the initial value will be recreated exactly once per store (unless using something like `jotai-scope`, which fractures a store into smaller universes).
72+
73+
```ts
74+
type RGB = [number, number, number];
75+
76+
function randomRGB(): RGB {
77+
...
78+
}
79+
80+
const lift = (value: number) => ([r, g, b]: RGB) => {
81+
return [r + value, g + value, b + value]
82+
}
83+
84+
const colorAtom = lazyAtom(randomRGB)
85+
86+
let store = createStore()
87+
88+
console.log(store.get(colorAtom)) // [0, 36, 128]
89+
store.set(colorAtom, lift(8))
90+
console.log(store.get(colorAtom)) // [8, 44, 136]
91+
92+
// recreating store, sometimes done when logging out or resetting the app in some way
93+
store = createStore()
94+
95+
console.log(store.get(colorAtom)) // [255, 12, 46] -- a new random color
96+
```

src/vanilla/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export { atomWithObservable } from './utils/atomWithObservable.ts'
1515
export { loadable } from './utils/loadable.ts'
1616
export { unwrap } from './utils/unwrap.ts'
1717
export { atomWithRefresh } from './utils/atomWithRefresh.ts'
18+
export { atomWithLazy } from './utils/atomWithLazy.ts'

src/vanilla/utils/atomWithLazy.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PrimitiveAtom, atom } from '../../vanilla.ts'
2+
3+
export function atomWithLazy<Value>(
4+
makeInitial: () => Value,
5+
): PrimitiveAtom<Value> {
6+
return {
7+
...atom(undefined as unknown as Value),
8+
get init() {
9+
return makeInitial()
10+
},
11+
}
12+
}
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { expect, it, vi } from 'vitest'
2+
import { createStore } from 'jotai/vanilla'
3+
import { atomWithLazy } from 'jotai/vanilla/utils'
4+
5+
it('initializes on first store get', async () => {
6+
const storeA = createStore()
7+
const storeB = createStore()
8+
9+
let externalState = 'first'
10+
const initializer = vi.fn(() => externalState)
11+
const anAtom = atomWithLazy(initializer)
12+
13+
expect(initializer).not.toHaveBeenCalled()
14+
expect(storeA.get(anAtom)).toEqual('first')
15+
expect(initializer).toHaveBeenCalledOnce()
16+
17+
externalState = 'second'
18+
19+
expect(storeA.get(anAtom)).toEqual('first')
20+
expect(initializer).toHaveBeenCalledOnce()
21+
expect(storeB.get(anAtom)).toEqual('second')
22+
expect(initializer).toHaveBeenCalledTimes(2)
23+
})
24+
25+
it('is writable', async () => {
26+
const store = createStore()
27+
const anAtom = atomWithLazy(() => 0)
28+
29+
store.set(anAtom, 123)
30+
31+
expect(store.get(anAtom)).toEqual(123)
32+
})
33+
34+
it('should work with a set state action', async () => {
35+
const store = createStore()
36+
const anAtom = atomWithLazy(() => 4)
37+
38+
store.set(anAtom, (prev: number) => prev * prev)
39+
40+
expect(store.get(anAtom)).toEqual(16)
41+
})

0 commit comments

Comments
 (0)