Skip to content

Commit 2bddd72

Browse files
committed
fix: full rewrite of ScopeProvider to address known issues
- fixes: #25, #36
1 parent 5c8bbcd commit 2bddd72

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+11042
-22
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
*~
22
*.swp
33
.vscode
4+
.DS_Store
45
dist
56
jotai
67
node_modules
7-
.DS_Store

approaches/readAtomState.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Objectives
2+
3+
1. Derived atoms are copied even if they don’t depend on scoped atoms.
4+
2. If the derived atom has already mounted, don't call onMount again.
5+
Fixes:
6+
7+
- [Scope caused atomWithObservable to be out of sync](https://github.com/jotaijs/jotai-scope/issues/36)
8+
- [Computed atoms get needlessly triggered again](https://github.com/jotaijs/jotai-scope/issues/25)
9+
10+
## Requirements
11+
12+
1. Some way to get whether the atom has been mounted.
13+
2. Some way to bypass the onMount call if the atom is already mounted.

approaches/unstable_derive.md

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Objectives
2+
3+
1. Derived atoms are not copied if they don’t depend on scoped atoms.
4+
2. When a derived atom starts depending on a scoped atom, a new atom state is created as the scoped atom state.
5+
3. When a derived atom stops depending on a scoped atom, it must be removed from the scope state and restored to the original atom state.
6+
a. When changing between scoped and unscoped, all subscibers must be notified.
7+
8+
Fixes:
9+
10+
- [Scope caused atomWithObservable to be out of sync](https://github.com/jotaijs/jotai-scope/issues/36)
11+
- [Computed atoms get needlessly triggered again](https://github.com/jotaijs/jotai-scope/issues/25)
12+
13+
# Requirements
14+
15+
1. Some way to track dependencies of computed atoms not in the scope without copying them.
16+
2. Some way to get whether the atom has been mounted.
17+
18+
# Problem Statement
19+
20+
A computed atom may or may not consume scoped atoms. This may also change as state changes.
21+
22+
```tsx
23+
const providerAtom = atom('unscoped')
24+
const scopedProviderAtom = atom('scoped')
25+
const shouldConsumeScopedAtom = atom(false)
26+
const consumerAtom = atom((get) => {
27+
if (get(shouldConsumeScopedAtom)) {
28+
return get(scopedProviderAtom)
29+
}
30+
return get(providerAtom)
31+
})
32+
33+
function Component() {
34+
const value = useAtomValue(consumerAtom)
35+
return value
36+
}
37+
38+
function App() {
39+
const setShouldConsumeScopedAtom = useSetAtom(shouldConsumeScopedAtom)
40+
useEffect(() => {
41+
const timeoutId = setTimeout(setShouldConsumeScopedAtom, 1000, true)
42+
return () => clearTimeout(timeoutId)
43+
}, [])
44+
45+
return (
46+
<ScopeProvider atoms={[scopedProviderAtom]}>
47+
<Component />
48+
</ScopeProvider>
49+
)
50+
}
51+
```
52+
53+
To properly handle `consumerAtom`, we need to track the dependencies of the computed atom.
54+
55+
# Proxy State
56+
57+
Atom state has the following shape;
58+
59+
```ts
60+
type AtomState = {
61+
d: Map<AnyAtom, number>; // map of atom consumers to their epoch number
62+
p: Set<AnyAtom>; // set of pending atom consumers
63+
n: number; // epoch number
64+
m?: {
65+
l: Set<() => void>; // set of listeners
66+
d: Set<AnyAtom>; // set of mounted atom consumers
67+
t: Set<AnyAtom>; // set of mounted atom providers
68+
u?: (setSelf: () => any) => (void | () => void); // unmount function
69+
};
70+
v?: any; // value
71+
e?: any; // error
72+
};
73+
```
74+
75+
All computed atoms (`atom.read !== defaultRead`) will have their base atomState converted to a proxy state. The proxy state will track dependencies and notify when they change.
76+
77+
0. Update all computed atoms with a proxy state in the parent store.
78+
1. If a computer atom does not depend on any scoped atoms, remove it from the unscopedComputed set
79+
2. If a computed atom starts depending on a scoped atom, add it to the scopedComputed set.
80+
a. If the scoped state does not already exist, create a new scoped atom state.
81+
3. If a computed atom stops depending on a scoped atom, remove it from the scopedComputed set.

eslint.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default tseslint.config(
2727
eqeqeq: 'error',
2828
'no-console': 'off',
2929
'no-inner-declarations': 'off',
30+
'no-sparse-arrays': 'off',
3031
'no-var': 'error',
3132
'prefer-const': 'error',
3233
'sort-imports': [

jotai/Provider.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createContext, createElement, useContext, useRef } from 'react'
2+
import type { FunctionComponent, ReactElement, ReactNode } from 'react'
3+
import { createStore, getDefaultStore } from './store'
4+
5+
type Store = ReturnType<typeof createStore>
6+
7+
type StoreContextType = ReturnType<typeof createContext<Store | undefined>>
8+
const StoreContext: StoreContextType = createContext<Store | undefined>(
9+
undefined
10+
)
11+
12+
type Options = {
13+
store?: Store
14+
}
15+
16+
export const useStore = (options?: Options): Store => {
17+
const store = useContext(StoreContext)
18+
return options?.store || store || getDefaultStore()
19+
}
20+
21+
// TODO should we consider using useState instead of useRef?
22+
export const Provider = ({
23+
children,
24+
store,
25+
}: {
26+
children?: ReactNode
27+
store?: Store
28+
}): ReactElement<
29+
{ value: Store | undefined },
30+
FunctionComponent<{ value: Store | undefined }>
31+
> => {
32+
const storeRef = useRef<Store>(undefined!)
33+
if (!store && !storeRef.current) {
34+
storeRef.current = createStore()
35+
}
36+
return createElement(
37+
StoreContext.Provider,
38+
{
39+
value: store || storeRef.current,
40+
},
41+
children
42+
)
43+
}

jotai/atom.ts

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type { INTERNAL_PrdStore as Store } from './store'
2+
3+
type Getter = <Value>(atom: Atom<Value>) => Value
4+
5+
type Setter = <Value, Args extends unknown[], Result>(
6+
atom: WritableAtom<Value, Args, Result>,
7+
...args: Args
8+
) => Result
9+
10+
type SetAtom<Args extends unknown[], Result> = <A extends Args>(
11+
...args: A
12+
) => Result
13+
14+
/**
15+
* setSelf is for internal use only and subject to change without notice.
16+
*/
17+
type Read<Value, SetSelf = never> = (
18+
get: Getter,
19+
options: { readonly signal: AbortSignal; readonly setSelf: SetSelf }
20+
) => Value
21+
22+
type Write<Args extends unknown[], Result> = (
23+
get: Getter,
24+
set: Setter,
25+
...args: Args
26+
) => Result
27+
28+
// This is an internal type and not part of public API.
29+
// Do not depend on it as it can change without notice.
30+
type WithInitialValue<Value> = {
31+
init: Value
32+
}
33+
34+
type OnUnmount = () => void
35+
36+
type OnMount<Args extends unknown[], Result> = <
37+
S extends SetAtom<Args, Result>,
38+
>(
39+
setAtom: S
40+
) => OnUnmount | void
41+
42+
export interface Atom<Value> {
43+
toString: () => string
44+
read: Read<Value>
45+
unstable_is?(a: Atom<unknown>): boolean
46+
debugLabel?: string
47+
/**
48+
* To ONLY be used by Jotai libraries to mark atoms as private. Subject to change.
49+
* @private
50+
*/
51+
debugPrivate?: boolean
52+
/**
53+
* Fires after atom is referenced by the store for the first time
54+
* This is still an experimental API and subject to change without notice.
55+
*/
56+
unstable_onInit?: (store: Store) => void
57+
}
58+
59+
export interface WritableAtom<Value, Args extends unknown[], Result>
60+
extends Atom<Value> {
61+
read: Read<Value, SetAtom<Args, Result>>
62+
write: Write<Args, Result>
63+
onMount?: OnMount<Args, Result>
64+
}
65+
66+
type SetStateAction<Value> = Value | ((prev: Value) => Value)
67+
68+
export type PrimitiveAtom<Value> = WritableAtom<
69+
Value,
70+
[SetStateAction<Value>],
71+
void
72+
>
73+
74+
let keyCount = 0 // global key count for all atoms
75+
76+
// writable derived atom
77+
export function atom<Value, Args extends unknown[], Result>(
78+
read: Read<Value, SetAtom<Args, Result>>,
79+
write: Write<Args, Result>
80+
): WritableAtom<Value, Args, Result>
81+
82+
// read-only derived atom
83+
export function atom<Value>(read: Read<Value>): Atom<Value>
84+
85+
// write-only derived atom
86+
export function atom<Value, Args extends unknown[], Result>(
87+
initialValue: Value,
88+
write: Write<Args, Result>
89+
): WritableAtom<Value, Args, Result> & WithInitialValue<Value>
90+
91+
// primitive atom without initial value
92+
export function atom<Value>(): PrimitiveAtom<Value | undefined> &
93+
WithInitialValue<Value | undefined>
94+
95+
// primitive atom
96+
export function atom<Value>(
97+
initialValue: Value
98+
): PrimitiveAtom<Value> & WithInitialValue<Value>
99+
100+
export function atom<Value, Args extends unknown[], Result>(
101+
read?: Value | Read<Value, SetAtom<Args, Result>>,
102+
write?: Write<Args, Result>
103+
) {
104+
const key = `atom${++keyCount}`
105+
const config = {
106+
toString() {
107+
return process.env?.MODE !== 'production' && this.debugLabel
108+
? key + ':' + this.debugLabel
109+
: key
110+
},
111+
} as WritableAtom<Value, Args, Result> & { init?: Value | undefined }
112+
if (typeof read === 'function') {
113+
config.read = read as Read<Value, SetAtom<Args, Result>>
114+
} else {
115+
config.init = read
116+
config.read = defaultRead
117+
config.write = defaultWrite as unknown as Write<Args, Result>
118+
}
119+
if (write) {
120+
config.write = write
121+
}
122+
return config
123+
}
124+
125+
function defaultRead<Value>(this: Atom<Value>, get: Getter) {
126+
return get(this)
127+
}
128+
129+
function defaultWrite<Value>(
130+
this: PrimitiveAtom<Value>,
131+
get: Getter,
132+
set: Setter,
133+
arg: SetStateAction<Value>
134+
) {
135+
return set(
136+
this,
137+
typeof arg === 'function' ? (arg as (prev: Value) => Value)(get(this)) : arg
138+
)
139+
}

0 commit comments

Comments
 (0)