Skip to content

Commit c412d03

Browse files
dai-shiMathis Møller
andauthored
refactor(core): re-implement useAtom with useReducer (#687)
* wip: use-sync-external-store * use reducer just like that * fix: avoid reading atom value twice * fix useAtomsSnapshot * update size snapshot * a workaround for react devtools * update size snapshot * empty commit * Update src/devtools/useAtomsSnapshot.ts Co-authored-by: Mathis Møller <mmm@cryosinternational.com> * Update src/devtools/useAtomsSnapshot.ts Co-authored-by: Mathis Møller <mmm@cryosinternational.com> * Update src/devtools/useAtomsSnapshot.ts Co-authored-by: Mathis Møller <mmm@cryosinternational.com> Co-authored-by: Mathis Møller <mmm@cryosinternational.com>
1 parent f9f425b commit c412d03

9 files changed

Lines changed: 102 additions & 218 deletions

File tree

.size-snapshot.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"index.js": {
3-
"bundled": 22130,
4-
"minified": 8884,
5-
"gzipped": 3283,
3+
"bundled": 19763,
4+
"minified": 8215,
5+
"gzipped": 3043,
66
"treeshaked": {
77
"rollup": {
88
"code": 14,
@@ -14,9 +14,9 @@
1414
}
1515
},
1616
"index.mjs": {
17-
"bundled": 22130,
18-
"minified": 8884,
19-
"gzipped": 3283,
17+
"bundled": 19763,
18+
"minified": 8215,
19+
"gzipped": 3043,
2020
"treeshaked": {
2121
"rollup": {
2222
"code": 14,
@@ -56,9 +56,9 @@
5656
}
5757
},
5858
"devtools.js": {
59-
"bundled": 20426,
60-
"minified": 8380,
61-
"gzipped": 3171,
59+
"bundled": 20062,
60+
"minified": 8222,
61+
"gzipped": 3114,
6262
"treeshaked": {
6363
"rollup": {
6464
"code": 28,
@@ -70,9 +70,9 @@
7070
}
7171
},
7272
"devtools.mjs": {
73-
"bundled": 20426,
74-
"minified": 8380,
75-
"gzipped": 3171,
73+
"bundled": 20062,
74+
"minified": 8222,
75+
"gzipped": 3114,
7676
"treeshaked": {
7777
"rollup": {
7878
"code": 28,

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
"tests/**/*.{js,ts,tsx}"
171171
]
172172
},
173+
"dependencies": {},
173174
"devDependencies": {
174175
"@babel/core": "^7.15.0",
175176
"@babel/plugin-transform-react-jsx": "^7.14.9",

src/core/Provider.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { createElement, useCallback, useDebugValue, useRef } from 'react'
1+
import {
2+
createElement,
3+
useDebugValue,
4+
useEffect,
5+
useRef,
6+
useState,
7+
} from 'react'
28
import type { PropsWithChildren } from 'react'
39
import type { Atom, Scope } from './atom'
410
import {
@@ -10,7 +16,6 @@ import {
1016
import type { ScopeContainerForDevelopment } from './contexts'
1117
import { DEV_GET_ATOM_STATE, DEV_GET_MOUNTED } from './store'
1218
import type { AtomState, Store } from './store'
13-
import { useMutableSource } from './useMutableSource'
1419

1520
export const Provider = ({
1621
initialValues,
@@ -40,9 +45,7 @@ export const Provider = ({
4045
return createElement(
4146
ScopeContainerContext.Provider,
4247
{
43-
value: scopeContainerRef.current as ReturnType<
44-
typeof createScopeContainer
45-
>,
48+
value: scopeContainerRef.current,
4649
},
4750
children
4851
)
@@ -75,11 +78,14 @@ const stateToPrintable = ([store, atoms]: [Store, Atom<unknown>[]]) =>
7578
// We keep a reference to the atoms in Provider's registeredAtoms in dev mode,
7679
// so atoms aren't garbage collected by the WeakMap of mounted atoms
7780
const useDebugState = (scopeContainer: ScopeContainerForDevelopment) => {
78-
const [store, , devMutableSource, devSubscribe] = scopeContainer
79-
const atoms = useMutableSource(
80-
devMutableSource,
81-
useCallback((devContainer) => devContainer.atoms, []),
82-
devSubscribe
83-
)
81+
const [store, devStore] = scopeContainer
82+
const [atoms, setAtoms] = useState(devStore.atoms)
83+
useEffect(() => {
84+
// HACK creating a new reference for useDebugValue to update
85+
const callback = () => setAtoms([...devStore.atoms])
86+
const unsubscribe = devStore.subscribe(callback)
87+
callback()
88+
return unsubscribe
89+
}, [devStore])
8490
useDebugValue([store, atoms], stateToPrintable)
8591
}

src/core/contexts.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,46 @@
11
import { createContext } from 'react'
22
import type { Context } from 'react'
33
import type { Atom, Scope } from './atom'
4-
import { GET_VERSION, createStore } from './store'
5-
import { createMutableSource } from './useMutableSource'
4+
import { createStore } from './store'
65

76
const createScopeContainerForProduction = (
87
initialValues?: Iterable<readonly [Atom<unknown>, unknown]>
98
) => {
109
const store = createStore(initialValues)
11-
const mutableSource = createMutableSource(store, store[GET_VERSION])
12-
return [store, mutableSource] as const
10+
return [store] as const
1311
}
1412

1513
const createScopeContainerForDevelopment = (
1614
initialValues?: Iterable<readonly [Atom<unknown>, unknown]>
1715
) => {
18-
let devVersion = 0
19-
const devListeners = new Set<() => void>()
20-
const devContainer = {
16+
const devStore = {
17+
listeners: new Set<() => void>(),
18+
subscribe: (callback: () => void) => {
19+
devStore.listeners.add(callback)
20+
return () => {
21+
devStore.listeners.delete(callback)
22+
}
23+
},
2124
atoms: Array.from(initialValues ?? []).map(([a]) => a),
2225
}
2326
const stateListener = (updatedAtom: Atom<unknown>, isNewAtom: boolean) => {
24-
++devVersion
2527
if (isNewAtom) {
2628
// FIXME memory leak
2729
// we should probably remove unmounted atoms eventually
28-
devContainer.atoms = [...devContainer.atoms, updatedAtom]
30+
devStore.atoms = [...devStore.atoms, updatedAtom]
2931
}
3032
Promise.resolve().then(() => {
31-
devListeners.forEach((listener) => listener())
33+
devStore.listeners.forEach((listener) => listener())
3234
})
3335
}
3436
const store = createStore(initialValues, stateListener)
35-
const mutableSource = createMutableSource(store, store[GET_VERSION])
36-
const devMutableSource = createMutableSource(devContainer, () => devVersion)
37-
const devSubscribe = (_: unknown, callback: () => void) => {
38-
devListeners.add(callback)
39-
return () => devListeners.delete(callback)
40-
}
41-
return [store, mutableSource, devMutableSource, devSubscribe] as const
37+
return [store, devStore] as const
4238
}
4339

4440
export const isDevScopeContainer = (
4541
scopeContainer: ScopeContainer
4642
): scopeContainer is ScopeContainerForDevelopment => {
47-
return scopeContainer.length > 2
43+
return scopeContainer.length > 1
4844
}
4945

5046
type ScopeContainerForProduction = ReturnType<

src/core/store.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ type Mounted = {
6666
type StateListener = (updatedAtom: AnyAtom, isNewAtom: boolean) => void
6767

6868
// store methods
69-
export const GET_VERSION = 'v'
7069
export const READ_ATOM = 'r'
7170
export const WRITE_ATOM = 'w'
7271
export const FLUSH_PENDING = 'f'
@@ -79,7 +78,6 @@ export const createStore = (
7978
initialValues?: Iterable<readonly [AnyAtom, unknown]>,
8079
stateListener?: StateListener
8180
) => {
82-
let version = 0
8381
const atomStateMap = new WeakMap<AnyAtom, AtomState>()
8482
const mountedMap = new WeakMap<AnyAtom, Mounted>()
8583
const pendingMap = new Map<AnyAtom, ReadDependencies | undefined>()
@@ -142,6 +140,9 @@ export const createStore = (
142140
if (!('v' in atomState) || !Object.is(atomState.v, value)) {
143141
atomState.v = value
144142
++atomState.r // increment revision
143+
if (atomState.d.has(atom)) {
144+
atomState.d.set(atom, atomState.r)
145+
}
145146
}
146147
commitAtomState(atom, atomState, dependencies && prevDependencies)
147148
}
@@ -569,7 +570,6 @@ export const createStore = (
569570
if (stateListener) {
570571
stateListener(atom, isNewAtom)
571572
}
572-
++version
573573
if (!pendingMap.has(atom)) {
574574
pendingMap.set(atom, prevDependencies)
575575
}
@@ -619,7 +619,6 @@ export const createStore = (
619619

620620
if (typeof process === 'object' && process.env.NODE_ENV !== 'production') {
621621
return {
622-
[GET_VERSION]: () => version,
623622
[READ_ATOM]: readAtom,
624623
[WRITE_ATOM]: writeAtom,
625624
[FLUSH_PENDING]: flushPending,
@@ -630,7 +629,6 @@ export const createStore = (
630629
}
631630
}
632631
return {
633-
[GET_VERSION]: () => version,
634632
[READ_ATOM]: readAtom,
635633
[WRITE_ATOM]: writeAtom,
636634
[FLUSH_PENDING]: flushPending,

src/core/useAtom.ts

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import { useCallback, useContext, useDebugValue, useEffect } from 'react'
1+
import {
2+
useCallback,
3+
useContext,
4+
useDebugValue,
5+
useEffect,
6+
useReducer,
7+
} from 'react'
28
import type { Atom, Scope, SetAtom, WritableAtom } from './atom'
39
import { getScopeContext } from './contexts'
410
import { FLUSH_PENDING, READ_ATOM, SUBSCRIBE_ATOM, WRITE_ATOM } from './store'
5-
import type { Store } from './store'
6-
import { useMutableSource } from './useMutableSource'
711

812
const isWritable = <Value, Update>(
913
atom: Atom<Value> | WritableAtom<Value, Update>
@@ -41,32 +45,6 @@ export function useAtom<Value, Update>(
4145
atom: Atom<Value> | WritableAtom<Value, Update>,
4246
scope?: Scope
4347
) {
44-
const getAtomValue = useCallback(
45-
(store: Store) => {
46-
const atomState = store[READ_ATOM](atom)
47-
if (atomState.e) {
48-
throw atomState.e // read error
49-
}
50-
if (atomState.p) {
51-
throw atomState.p // read promise
52-
}
53-
if (atomState.w) {
54-
throw atomState.w // write promise
55-
}
56-
if ('v' in atomState) {
57-
return atomState.v as Value
58-
}
59-
throw new Error('no atom value')
60-
},
61-
[atom]
62-
)
63-
64-
const subscribe = useCallback(
65-
(store: Store, callback: () => void) =>
66-
store[SUBSCRIBE_ATOM](atom, callback),
67-
[atom]
68-
)
69-
7048
if ('scope' in atom) {
7149
console.warn(
7250
'atom.scope is deprecated. Please do useAtom(atom, scope) instead.'
@@ -75,8 +53,33 @@ export function useAtom<Value, Update>(
7553
}
7654

7755
const ScopeContext = getScopeContext(scope)
78-
const [store, mutableSource] = useContext(ScopeContext)
79-
const value = useMutableSource(mutableSource, getAtomValue, subscribe)
56+
const [store] = useContext(ScopeContext)
57+
58+
const getAtomValue = useCallback(() => {
59+
const atomState = store[READ_ATOM](atom)
60+
if (atomState.e) {
61+
throw atomState.e // read error
62+
}
63+
if (atomState.p) {
64+
throw atomState.p // read promise
65+
}
66+
if (atomState.w) {
67+
throw atomState.w // write promise
68+
}
69+
if ('v' in atomState) {
70+
return atomState.v as Value
71+
}
72+
throw new Error('no atom value')
73+
}, [store, atom])
74+
75+
const [value, forceUpdate] = useReducer(getAtomValue, undefined, getAtomValue)
76+
77+
useEffect(() => {
78+
const unsubscribe = store[SUBSCRIBE_ATOM](atom, forceUpdate)
79+
forceUpdate()
80+
return unsubscribe
81+
}, [store, atom])
82+
8083
useEffect(() => {
8184
store[FLUSH_PENDING]()
8285
})

0 commit comments

Comments
 (0)