Skip to content

Commit 3764266

Browse files
authored
scoped stores contain their scope (#79)
1 parent 0d938c7 commit 3764266

File tree

6 files changed

+135
-145
lines changed

6 files changed

+135
-145
lines changed

src/ScopeProvider/ScopeProvider.tsx

Lines changed: 24 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,16 @@
1-
import {
2-
type PropsWithChildren,
3-
createContext,
4-
useContext,
5-
useEffect,
6-
useState,
7-
} from 'react'
1+
import { type PropsWithChildren, useEffect, useState } from 'react'
82
import { Provider, useStore } from 'jotai/react'
93
import { useHydrateAtoms } from 'jotai/utils'
10-
import type {
11-
AnyAtom,
12-
AnyAtomFamily,
13-
AtomDefault,
14-
Scope,
15-
Store,
4+
import {
5+
type AnyAtom,
6+
type AnyAtomFamily,
7+
type AtomDefault,
8+
SCOPE,
9+
ScopedStore,
10+
type Store,
1611
} from '../types'
17-
import { createPatchedStore, isTopLevelScope } from './patchedStore'
1812
import { createScope } from './scope'
1913

20-
const ScopeContext = createContext<{
21-
scope: Scope | undefined
22-
baseStore: Store | undefined
23-
}>({ scope: undefined, baseStore: undefined })
24-
2514
type ScopeProviderBaseProps = PropsWithChildren<{
2615
atoms?: Iterable<AnyAtom | AtomDefault>
2716
atomFamilies?: Iterable<AnyAtomFamily>
@@ -50,16 +39,9 @@ export function ScopeProvider({
5039
atoms: atomsOrTuples = [],
5140
atomFamilies,
5241
children,
53-
debugName,
42+
debugName: scopeName,
5443
}: ScopeProviderBaseProps) {
55-
const parentStore: Store = useStore()
56-
let { scope: parentScope, baseStore = parentStore } = useContext(ScopeContext)
57-
// if this ScopeProvider is the first descendant scope under Provider then it is the top level scope
58-
// https://github.com/jotaijs/jotai-scope/pull/33#discussion_r1604268003
59-
if (isTopLevelScope(parentStore)) {
60-
parentScope = undefined
61-
baseStore = parentStore
62-
}
44+
const parentStore: Store | ScopedStore = useStore()
6345

6446
const atoms = Array.from(atomsOrTuples, (a) => (Array.isArray(a) ? a[0] : a))
6547

@@ -68,19 +50,20 @@ export function ScopeProvider({
6850
const atomFamilySet = new Set(atomFamilies)
6951

7052
function initialize() {
71-
const scope = createScope(atomSet, atomFamilySet, parentScope, debugName)
7253
return {
73-
patchedStore: createPatchedStore(baseStore, scope),
74-
scopeContext: { scope, baseStore },
54+
scopedStore: createScope({
55+
atomSet,
56+
atomFamilySet,
57+
parentStore,
58+
scopeName,
59+
}),
7560
hasChanged(current: {
76-
baseStore: Store
77-
parentScope: Scope | undefined
61+
parentStore: Store | ScopedStore
7862
atomSet: Set<AnyAtom>
7963
atomFamilySet: Set<AnyAtomFamily>
8064
}) {
8165
return (
82-
parentScope !== current.parentScope ||
83-
baseStore !== current.baseStore ||
66+
parentStore !== current.parentStore ||
8467
!isEqualSet(atomSet, current.atomSet) ||
8568
!isEqualSet(atomFamilySet, current.atomFamilySet)
8669
)
@@ -89,21 +72,17 @@ export function ScopeProvider({
8972
}
9073

9174
const [state, setState] = useState(initialize)
92-
const { hasChanged, scopeContext, patchedStore } = state
93-
if (hasChanged({ parentScope, atomSet, atomFamilySet, baseStore })) {
94-
scopeContext.scope?.cleanup()
75+
const { hasChanged, scopedStore } = state
76+
if (hasChanged({ atomSet, atomFamilySet, parentStore })) {
77+
scopedStore[SCOPE].cleanup()
9578
setState(initialize)
9679
}
9780
useHydrateAtoms(
9881
Array.from(atomsOrTuples).filter(Array.isArray) as AtomDefault[],
99-
{ store: patchedStore }
100-
)
101-
useEffect(() => scopeContext.scope.cleanup, [scopeContext.scope])
102-
return (
103-
<ScopeContext.Provider value={scopeContext}>
104-
<Provider store={patchedStore}>{children}</Provider>
105-
</ScopeContext.Provider>
82+
{ store: scopedStore }
10683
)
84+
useEffect(() => scopedStore[SCOPE].cleanup, [scopedStore])
85+
return <Provider store={scopedStore}>{children}</Provider>
10786
}
10887

10988
function isEqualSet(a: Set<unknown>, b: Set<unknown>) {

src/ScopeProvider/patchedStore.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.

src/ScopeProvider/scope.ts

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
11
import { type Atom, atom } from 'jotai'
22
import { __DEV__ } from '../env'
3-
import type { AnyAtom, AnyAtomFamily, AnyWritableAtom, Scope } from '../types'
3+
import {
4+
type AnyAtom,
5+
type AnyAtomFamily,
6+
type AnyWritableAtom,
7+
SCOPE,
8+
type Scope,
9+
type ScopedStore,
10+
type Store,
11+
} from '../types'
412

513
const globalScopeKey: { name?: string } = {}
614
if (__DEV__) {
715
globalScopeKey.name = 'unscoped'
8-
globalScopeKey.toString = toString
16+
globalScopeKey.toString = toNameString
917
}
1018

1119
type GlobalScopeKey = typeof globalScopeKey
1220

13-
export function createScope(
14-
atoms: Set<AnyAtom> = new Set(),
15-
atomFamilies: Set<AnyAtomFamily> = new Set(),
16-
parentScope: Scope | undefined,
17-
scopeName?: string | undefined
18-
): Scope {
21+
export function createScope({
22+
atomSet = new Set(),
23+
atomFamilySet = new Set(),
24+
parentStore,
25+
scopeName,
26+
}: {
27+
atomSet?: Set<AnyAtom>
28+
atomFamilySet?: Set<AnyAtomFamily>
29+
parentStore: Store | ScopedStore
30+
scopeName?: string
31+
}): ScopedStore {
32+
const parentScope = SCOPE in parentStore ? parentStore[SCOPE] : undefined
1933
const explicit = new WeakMap<AnyAtom, [AnyAtom, Scope?]>()
2034
const implicit = new WeakMap<AnyAtom, [AnyAtom, Scope?]>()
2135
type ScopeMap = WeakMap<AnyAtom, [AnyAtom, Scope?]>
@@ -53,16 +67,16 @@ export function createScope(
5367

5468
if (scopeName && __DEV__) {
5569
currentScope.name = scopeName
56-
currentScope.toString = toString
70+
currentScope.toString = toNameString
5771
}
5872

5973
// populate explicitly scoped atoms
60-
for (const anAtom of atoms) {
74+
for (const anAtom of atomSet) {
6175
explicit.set(anAtom, [cloneAtom(anAtom, currentScope), currentScope])
6276
}
6377

6478
const cleanupFamiliesSet = new Set<() => void>()
65-
for (const atomFamily of atomFamilies) {
79+
for (const atomFamily of atomFamilySet) {
6680
for (const param of atomFamily.getParams()) {
6781
const anAtom = atomFamily(param)
6882
if (!explicit.has(anAtom)) {
@@ -72,7 +86,7 @@ export function createScope(
7286
const cleanupFamily = atomFamily.unstable_listen((e) => {
7387
if (e.type === 'CREATE' && !explicit.has(e.atom)) {
7488
explicit.set(e.atom, [cloneAtom(e.atom, currentScope), currentScope])
75-
} else if (!atoms.has(e.atom)) {
89+
} else if (!atomSet.has(e.atom)) {
7690
explicit.delete(e.atom)
7791
}
7892
})
@@ -245,7 +259,8 @@ export function createScope(
245259
}
246260
}
247261

248-
return currentScope
262+
const scopedStore = createPatchedStore(parentStore, currentScope)
263+
return scopedStore
249264
}
250265

251266
function isWritableAtom(anAtom: AnyAtom): anAtom is AnyWritableAtom {
@@ -254,7 +269,7 @@ function isWritableAtom(anAtom: AnyAtom): anAtom is AnyWritableAtom {
254269

255270
const { read: defaultRead, write: defaultWrite } = atom<unknown>(null)
256271

257-
function toString(this: { name: string }) {
272+
function toNameString(this: { name: string }) {
258273
return this.name
259274
}
260275

@@ -265,3 +280,46 @@ function combineVoidFunctions(...fns: (() => void)[]) {
265280
}
266281
}
267282
}
283+
284+
function PatchedStore() {}
285+
286+
/**
287+
* @returns a patched store that intercepts get and set calls to apply the scope
288+
*/
289+
function createPatchedStore(baseStore: Store, scope: Scope): ScopedStore {
290+
const store: ScopedStore = {
291+
...baseStore,
292+
get(anAtom, ...args) {
293+
const [scopedAtom] = scope.getAtom(anAtom)
294+
return baseStore.get(scopedAtom, ...args)
295+
},
296+
set(anAtom, ...args) {
297+
const [scopedAtom, implicitScope] = scope.getAtom(anAtom)
298+
const restore = scope.prepareWriteAtom(
299+
scopedAtom,
300+
anAtom,
301+
implicitScope,
302+
scope
303+
)
304+
try {
305+
return baseStore.set(scopedAtom, ...args)
306+
} finally {
307+
restore?.()
308+
}
309+
},
310+
sub(anAtom, ...args) {
311+
const [scopedAtom] = scope.getAtom(anAtom)
312+
return baseStore.sub(scopedAtom, ...args)
313+
},
314+
[SCOPE]: scope,
315+
// TODO: update this patch to support devtools
316+
}
317+
return Object.assign(Object.create(PatchedStore.prototype), store)
318+
}
319+
320+
/**
321+
* @returns true if the current scope is the first descendant scope under Provider
322+
*/
323+
export function isTopLevelScope(parentStore: Store) {
324+
return !(parentStore instanceof PatchedStore)
325+
}

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import type { AtomFamily } from 'jotai/vanilla/utils/atomFamily'
33

44
export type Store = ReturnType<typeof createStore>
55

6+
export type ScopedStore = Store & {
7+
[SCOPE]: Scope
8+
}
9+
610
export type AnyAtom = Atom<any> | AnyWritableAtom
711

812
export type AnyAtomFamily = AtomFamily<any, AnyAtom>
@@ -45,4 +49,6 @@ export type Scope = {
4549
toString?: () => string
4650
}
4751

52+
export const SCOPE = Symbol('scope')
53+
4854
export type AtomDefault = readonly [AnyWritableAtom, unknown]

tests/ScopeProvider/07_writable.test.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
} from 'jotai'
99
import { describe, expect, test } from 'vitest'
1010
import { ScopeProvider } from '../../src'
11-
import { createPatchedStore } from '../../src/ScopeProvider/patchedStore'
1211
import { createScope } from '../../src/ScopeProvider/scope'
1312
import { AnyAtom } from '../../src/types'
1413
import { clickButton, getTextContents } from '../utils'
@@ -183,17 +182,12 @@ describe('scope chains', () => {
183182
c.debugLabel = 'c'
184183
function createScopes(atoms: AnyAtom[] = []) {
185184
const s0 = createStore()
186-
const s1 = createPatchedStore(
187-
s0,
188-
createScope(
189-
...(Object.values({
190-
atoms: new Set(atoms),
191-
atomFamilies: undefined,
192-
parentScope: undefined,
193-
scopeName: 'S1',
194-
}) as Parameters<typeof createScope>)
195-
)
196-
)
185+
const s1 = createScope({
186+
atomSet: new Set(atoms),
187+
atomFamilySet: undefined,
188+
parentStore: s0,
189+
scopeName: 'S1',
190+
})
197191
return { s0, s1 }
198192
}
199193
test('S1[a]: a1, b0(,a1), c0(,b0(,a1))', () => {

0 commit comments

Comments
 (0)