Skip to content

Commit 725db10

Browse files
authored
Merge branch 'main' into atom-state-store-hook
2 parents 9b5b67f + d825c2a commit 725db10

File tree

4 files changed

+121
-84
lines changed

4 files changed

+121
-84
lines changed

src/vanilla/internals.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,9 @@ const recomputeInvalidatedAtoms: RecomputeInvalidatedAtoms = (store) => {
528528
}
529529
}
530530

531+
// Dev only
532+
const storeMutationSet = new WeakSet<Store>()
533+
531534
const readAtomState: ReadAtomState = (store, atom) => {
532535
const buildingBlocks = getInternalBuildingBlocks(store)
533536
const mountedMap = buildingBlocks[1]
@@ -637,7 +640,15 @@ const readAtomState: ReadAtomState = (store, atom) => {
637640
}
638641
const prevEpochNumber = atomState.n
639642
try {
643+
if (import.meta.env?.MODE !== 'production') {
644+
storeMutationSet.delete(store)
645+
}
640646
const valueOrPromise = atomRead(store, atom, getter, options as never)
647+
if (import.meta.env?.MODE !== 'production' && storeMutationSet.has(store)) {
648+
console.warn(
649+
'Detected store mutation during atom read. This is not supported.',
650+
)
651+
}
641652
setAtomStateValueOrPromise(store, atom, valueOrPromise)
642653
if (isPromiseLike(valueOrPromise)) {
643654
registerAbortHandler(valueOrPromise, () => controller?.abort())
@@ -705,6 +716,9 @@ const writeAtomState: WriteAtomState = (store, atom, ...args) => {
705716
// NOTE technically possible but restricted as it may cause bugs
706717
throw new Error('atom not writable')
707718
}
719+
if (import.meta.env?.MODE !== 'production') {
720+
storeMutationSet.add(store)
721+
}
708722
const prevEpochNumber = aState.n
709723
const v = args[0] as V
710724
setAtomStateValueOrPromise(store, a, v)

src/vanilla/utils/loadable.ts

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,51 +21,52 @@ export function loadable<Value>(anAtom: Atom<Value>): Atom<Loadable<Value>> {
2121
PromiseLike<Awaited<Value>>,
2222
Loadable<Value>
2323
>()
24-
const refreshAtom = atom(0)
24+
const refreshAtom = atom([() => {}, 0] as [() => void, number])
25+
refreshAtom.unstable_onInit = (store) => {
26+
store.set(refreshAtom, ([, c]) => [
27+
() => store.set(refreshAtom, ([f, c]) => [f, c + 1]),
28+
c,
29+
])
30+
}
2531

2632
if (import.meta.env?.MODE !== 'production') {
2733
refreshAtom.debugPrivate = true
2834
}
2935

30-
const derivedAtom = atom(
31-
(get, { setSelf }) => {
32-
get(refreshAtom)
33-
let value: Value
34-
try {
35-
value = get(anAtom)
36-
} catch (error) {
37-
return { state: 'hasError', error } as Loadable<Value>
38-
}
39-
if (!isPromiseLike<Value>(value)) {
40-
return { state: 'hasData', data: value } as Loadable<Value>
41-
}
42-
const promise = value
43-
const cached1 = loadableCache.get(promise)
44-
if (cached1) {
45-
return cached1
46-
}
47-
promise.then(
48-
(data) => {
49-
loadableCache.set(promise, { state: 'hasData', data })
50-
setSelf()
51-
},
52-
(error) => {
53-
loadableCache.set(promise, { state: 'hasError', error })
54-
setSelf()
55-
},
56-
)
36+
const derivedAtom = atom((get) => {
37+
const [triggerRefresh] = get(refreshAtom)
38+
let value: Value
39+
try {
40+
value = get(anAtom)
41+
} catch (error) {
42+
return { state: 'hasError', error } as Loadable<Value>
43+
}
44+
if (!isPromiseLike<Value>(value)) {
45+
return { state: 'hasData', data: value } as Loadable<Value>
46+
}
47+
const promise = value
48+
const cached1 = loadableCache.get(promise)
49+
if (cached1) {
50+
return cached1
51+
}
52+
promise.then(
53+
(data) => {
54+
loadableCache.set(promise, { state: 'hasData', data })
55+
triggerRefresh()
56+
},
57+
(error) => {
58+
loadableCache.set(promise, { state: 'hasError', error })
59+
triggerRefresh()
60+
},
61+
)
5762

58-
const cached2 = loadableCache.get(promise)
59-
if (cached2) {
60-
return cached2
61-
}
62-
loadableCache.set(promise, LOADING as Loadable<Value>)
63-
return LOADING as Loadable<Value>
64-
},
65-
(_get, set) => {
66-
set(refreshAtom, (c) => c + 1)
67-
},
68-
)
63+
const cached2 = loadableCache.get(promise)
64+
if (cached2) {
65+
return cached2
66+
}
67+
loadableCache.set(promise, LOADING as Loadable<Value>)
68+
return LOADING as Loadable<Value>
69+
})
6970

7071
if (import.meta.env?.MODE !== 'production') {
7172
derivedAtom.debugPrivate = true

src/vanilla/utils/unwrap.ts

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -47,57 +47,58 @@ export function unwrap<Value, Args extends unknown[], Result, PendingValue>(
4747
PromiseLike<unknown>,
4848
Awaited<Value>
4949
>()
50-
const refreshAtom = atom(0)
50+
const refreshAtom = atom([() => {}, 0] as [() => void, number])
51+
refreshAtom.unstable_onInit = (store) => {
52+
store.set(refreshAtom, ([, c]) => [
53+
() => store.set(refreshAtom, ([f, c]) => [f, c + 1]),
54+
c,
55+
])
56+
}
5157

5258
if (import.meta.env?.MODE !== 'production') {
5359
refreshAtom.debugPrivate = true
5460
}
5561

56-
const promiseAndValueAtom: WritableAtom<PromiseAndValue, [], void> & {
62+
const promiseAndValueAtom: Atom<PromiseAndValue> & {
5763
init?: undefined
58-
} = atom(
59-
(get, { setSelf }) => {
60-
get(refreshAtom)
61-
let prev: PromiseAndValue | undefined
62-
try {
63-
prev = get(promiseAndValueAtom) as PromiseAndValue | undefined
64-
} catch {
65-
// ignore previous errors to avoid getting stuck in error state
66-
}
67-
const promise = get(anAtom)
68-
if (!isPromiseLike(promise)) {
69-
return { v: promise as Awaited<Value> }
70-
}
71-
if (promise !== prev?.p) {
72-
promise.then(
73-
(v) => {
74-
promiseResultCache.set(promise, v as Awaited<Value>)
75-
setSelf()
76-
},
77-
(e) => {
78-
promiseErrorCache.set(promise, e)
79-
setSelf()
80-
},
81-
)
82-
}
83-
if (promiseErrorCache.has(promise)) {
84-
throw promiseErrorCache.get(promise)
85-
}
86-
if (promiseResultCache.has(promise)) {
87-
return {
88-
p: promise,
89-
v: promiseResultCache.get(promise) as Awaited<Value>,
90-
}
64+
} = atom((get) => {
65+
const [triggerRefresh] = get(refreshAtom)
66+
let prev: PromiseAndValue | undefined
67+
try {
68+
prev = get(promiseAndValueAtom) as PromiseAndValue | undefined
69+
} catch {
70+
// ignore previous errors to avoid getting stuck in error state
71+
}
72+
const promise = get(anAtom)
73+
if (!isPromiseLike(promise)) {
74+
return { v: promise as Awaited<Value> }
75+
}
76+
if (promise !== prev?.p) {
77+
promise.then(
78+
(v) => {
79+
promiseResultCache.set(promise, v as Awaited<Value>)
80+
triggerRefresh()
81+
},
82+
(e) => {
83+
promiseErrorCache.set(promise, e)
84+
triggerRefresh()
85+
},
86+
)
87+
}
88+
if (promiseErrorCache.has(promise)) {
89+
throw promiseErrorCache.get(promise)
90+
}
91+
if (promiseResultCache.has(promise)) {
92+
return {
93+
p: promise,
94+
v: promiseResultCache.get(promise) as Awaited<Value>,
9195
}
92-
if (prev && 'v' in prev) {
93-
return { p: promise, f: fallback(prev.v), v: prev.v }
94-
}
95-
return { p: promise, f: fallback() }
96-
},
97-
(_get, set) => {
98-
set(refreshAtom, (c) => c + 1)
99-
},
100-
)
96+
}
97+
if (prev && 'v' in prev) {
98+
return { p: promise, f: fallback(prev.v), v: prev.v }
99+
}
100+
return { p: promise, f: fallback() }
101+
})
101102
// HACK to read PromiseAndValue atom before initialization
102103
promiseAndValueAtom.init = undefined
103104

tests/vanilla/store.test.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { waitFor } from '@testing-library/react'
2-
import { assert, describe, expect, it, vi } from 'vitest'
2+
import { afterEach, assert, beforeEach, describe, expect, it, vi } from 'vitest'
33
import { atom, createStore } from 'jotai/vanilla'
44
import type { Atom, Getter, PrimitiveAtom } from 'jotai/vanilla'
55
import {
@@ -49,6 +49,15 @@ const deriveStore = (
4949
return derivedStore
5050
}
5151

52+
let savedConsoleWarn: any
53+
beforeEach(() => {
54+
savedConsoleWarn = console.warn
55+
console.warn = vi.fn()
56+
})
57+
afterEach(() => {
58+
console.warn = savedConsoleWarn
59+
})
60+
5261
it('should not fire on subscribe', async () => {
5362
const store = createStore()
5463
const countAtom = atom(0)
@@ -1270,3 +1279,15 @@ it('updates dependents when it eagerly recomputes dirty atoms', () => {
12701279

12711280
expect(store.get(activeCountAtom)).toBe(1)
12721281
})
1282+
1283+
it('[DEV-ONLY] should warn store mutation during read', () => {
1284+
const store = createStore()
1285+
const countAtom = atom(0)
1286+
const derivedAtom = atom(() => {
1287+
store.set(countAtom, (c) => c + 1)
1288+
})
1289+
store.get(derivedAtom)
1290+
expect(console.warn).toHaveBeenCalledWith(
1291+
'Detected store mutation during atom read. This is not supported.',
1292+
)
1293+
})

0 commit comments

Comments
 (0)