-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: full rewrite of ScopeProvider to address known issues
- Loading branch information
Showing
70 changed files
with
10,846 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
*~ | ||
*.swp | ||
.vscode | ||
.DS_Store | ||
dist | ||
jotai | ||
node_modules | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
## Objectives | ||
|
||
1. Derived atoms are copied even if they don’t depend on scoped atoms. | ||
2. If the derived atom has already mounted, don't call onMount again. | ||
Fixes: | ||
|
||
- [Scope caused atomWithObservable to be out of sync](https://github.com/jotaijs/jotai-scope/issues/36) | ||
- [Computed atoms get needlessly triggered again](https://github.com/jotaijs/jotai-scope/issues/25) | ||
|
||
## Requirements | ||
|
||
1. Some way to get whether the atom has been mounted. | ||
2. Some way to bypass the onMount call if the atom is already mounted. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# Objectives | ||
|
||
1. Derived atoms are not copied if they don’t depend on scoped atoms. | ||
2. When a derived atom starts depending on a scoped atom, a new atom state is created as the scoped atom state. | ||
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. | ||
a. When changing between scoped and unscoped, all subscibers must be notified. | ||
|
||
Fixes: | ||
|
||
- [Scope caused atomWithObservable to be out of sync](https://github.com/jotaijs/jotai-scope/issues/36) | ||
- [Computed atoms get needlessly triggered again](https://github.com/jotaijs/jotai-scope/issues/25) | ||
|
||
# Requirements | ||
|
||
1. Some way to track dependencies of computed atoms not in the scope without copying them. | ||
2. Some way to get whether the atom has been mounted. | ||
|
||
# Problem Statement | ||
|
||
A computed atom may or may not consume scoped atoms. This may also change as state changes. | ||
|
||
```tsx | ||
const providerAtom = atom('unscoped') | ||
const scopedProviderAtom = atom('scoped') | ||
const shouldConsumeScopedAtom = atom(false) | ||
const consumerAtom = atom((get) => { | ||
if (get(shouldConsumeScopedAtom)) { | ||
return get(scopedProviderAtom) | ||
} | ||
return get(providerAtom) | ||
}) | ||
|
||
function Component() { | ||
const value = useAtomValue(consumerAtom) | ||
return value | ||
} | ||
|
||
function App() { | ||
const setShouldConsumeScopedAtom = useSetAtom(shouldConsumeScopedAtom) | ||
useEffect(() => { | ||
const timeoutId = setTimeout(setShouldConsumeScopedAtom, 1000, true) | ||
return () => clearTimeout(timeoutId) | ||
}, []) | ||
|
||
return ( | ||
<ScopeProvider atoms={[scopedProviderAtom]}> | ||
<Component /> | ||
</ScopeProvider> | ||
) | ||
} | ||
``` | ||
|
||
To properly handle `consumerAtom`, we need to track the dependencies of the computed atom. | ||
|
||
# Proxy State | ||
|
||
Atom state has the following shape; | ||
|
||
```ts | ||
type AtomState = { | ||
d: Map<AnyAtom, number>; // map of atom consumers to their epoch number | ||
p: Set<AnyAtom>; // set of pending atom consumers | ||
n: number; // epoch number | ||
m?: { | ||
l: Set<() => void>; // set of listeners | ||
d: Set<AnyAtom>; // set of mounted atom consumers | ||
t: Set<AnyAtom>; // set of mounted atom providers | ||
u?: (setSelf: () => any) => (void | () => void); // unmount function | ||
}; | ||
v?: any; // value | ||
e?: any; // error | ||
}; | ||
``` | ||
|
||
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. | ||
|
||
0. Update all computed atoms with a proxy state in the parent store. | ||
1. If a computer atom does not depend on any scoped atoms, remove it from the unscopedComputed set | ||
2. If a computed atom starts depending on a scoped atom, add it to the scopedComputed set. | ||
a. If the scoped state does not already exist, create a new scoped atom state. | ||
3. If a computed atom stops depending on a scoped atom, remove it from the scopedComputed set. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { createContext, createElement, useContext, useRef } from 'react' | ||
import type { FunctionComponent, ReactElement, ReactNode } from 'react' | ||
import { createStore, getDefaultStore } from './store' | ||
|
||
type Store = ReturnType<typeof createStore> | ||
|
||
type StoreContextType = ReturnType<typeof createContext<Store | undefined>> | ||
const StoreContext: StoreContextType = createContext<Store | undefined>( | ||
undefined | ||
) | ||
|
||
type Options = { | ||
store?: Store | ||
} | ||
|
||
export const useStore = (options?: Options): Store => { | ||
const store = useContext(StoreContext) | ||
return options?.store || store || getDefaultStore() | ||
} | ||
|
||
// TODO should we consider using useState instead of useRef? | ||
export const Provider = ({ | ||
children, | ||
store, | ||
}: { | ||
children?: ReactNode | ||
store?: Store | ||
}): ReactElement< | ||
{ value: Store | undefined }, | ||
FunctionComponent<{ value: Store | undefined }> | ||
> => { | ||
const storeRef = useRef<Store>(undefined!) | ||
if (!store && !storeRef.current) { | ||
storeRef.current = createStore() | ||
} | ||
return createElement( | ||
StoreContext.Provider, | ||
{ | ||
value: store || storeRef.current, | ||
}, | ||
children | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import type { INTERNAL_PrdStore as Store } from './store' | ||
|
||
type Getter = <Value>(atom: Atom<Value>) => Value | ||
|
||
type Setter = <Value, Args extends unknown[], Result>( | ||
atom: WritableAtom<Value, Args, Result>, | ||
...args: Args | ||
) => Result | ||
|
||
type SetAtom<Args extends unknown[], Result> = <A extends Args>( | ||
...args: A | ||
) => Result | ||
|
||
/** | ||
* setSelf is for internal use only and subject to change without notice. | ||
*/ | ||
type Read<Value, SetSelf = never> = ( | ||
get: Getter, | ||
options: { readonly signal: AbortSignal; readonly setSelf: SetSelf } | ||
) => Value | ||
|
||
type Write<Args extends unknown[], Result> = ( | ||
get: Getter, | ||
set: Setter, | ||
...args: Args | ||
) => Result | ||
|
||
// This is an internal type and not part of public API. | ||
// Do not depend on it as it can change without notice. | ||
type WithInitialValue<Value> = { | ||
init: Value | ||
} | ||
|
||
type OnUnmount = () => void | ||
|
||
type OnMount<Args extends unknown[], Result> = < | ||
S extends SetAtom<Args, Result>, | ||
>( | ||
setAtom: S | ||
) => OnUnmount | void | ||
|
||
export interface Atom<Value> { | ||
toString: () => string | ||
read: Read<Value> | ||
unstable_is?(a: Atom<unknown>): boolean | ||
debugLabel?: string | ||
/** | ||
* To ONLY be used by Jotai libraries to mark atoms as private. Subject to change. | ||
* @private | ||
*/ | ||
debugPrivate?: boolean | ||
/** | ||
* Fires after atom is referenced by the store for the first time | ||
* This is still an experimental API and subject to change without notice. | ||
*/ | ||
unstable_onInit?: (store: Store) => void | ||
} | ||
|
||
export interface WritableAtom<Value, Args extends unknown[], Result> | ||
extends Atom<Value> { | ||
read: Read<Value, SetAtom<Args, Result>> | ||
write: Write<Args, Result> | ||
onMount?: OnMount<Args, Result> | ||
} | ||
|
||
type SetStateAction<Value> = Value | ((prev: Value) => Value) | ||
|
||
export type PrimitiveAtom<Value> = WritableAtom< | ||
Value, | ||
[SetStateAction<Value>], | ||
void | ||
> | ||
|
||
let keyCount = 0 // global key count for all atoms | ||
|
||
// writable derived atom | ||
export function atom<Value, Args extends unknown[], Result>( | ||
read: Read<Value, SetAtom<Args, Result>>, | ||
write: Write<Args, Result> | ||
): WritableAtom<Value, Args, Result> | ||
|
||
// read-only derived atom | ||
export function atom<Value>(read: Read<Value>): Atom<Value> | ||
|
||
// write-only derived atom | ||
export function atom<Value, Args extends unknown[], Result>( | ||
initialValue: Value, | ||
write: Write<Args, Result> | ||
): WritableAtom<Value, Args, Result> & WithInitialValue<Value> | ||
|
||
// primitive atom without initial value | ||
export function atom<Value>(): PrimitiveAtom<Value | undefined> & | ||
WithInitialValue<Value | undefined> | ||
|
||
// primitive atom | ||
export function atom<Value>( | ||
initialValue: Value | ||
): PrimitiveAtom<Value> & WithInitialValue<Value> | ||
|
||
export function atom<Value, Args extends unknown[], Result>( | ||
read?: Value | Read<Value, SetAtom<Args, Result>>, | ||
write?: Write<Args, Result> | ||
) { | ||
const key = `atom${++keyCount}` | ||
const config = { | ||
toString() { | ||
return process.env?.MODE !== 'production' && this.debugLabel | ||
? key + ':' + this.debugLabel | ||
: key | ||
}, | ||
} as WritableAtom<Value, Args, Result> & { init?: Value | undefined } | ||
if (typeof read === 'function') { | ||
config.read = read as Read<Value, SetAtom<Args, Result>> | ||
} else { | ||
config.init = read | ||
config.read = defaultRead | ||
config.write = defaultWrite as unknown as Write<Args, Result> | ||
} | ||
if (write) { | ||
config.write = write | ||
} | ||
return config | ||
} | ||
|
||
function defaultRead<Value>(this: Atom<Value>, get: Getter) { | ||
return get(this) | ||
} | ||
|
||
function defaultWrite<Value>( | ||
this: PrimitiveAtom<Value>, | ||
get: Getter, | ||
set: Setter, | ||
arg: SetStateAction<Value> | ||
) { | ||
return set( | ||
this, | ||
typeof arg === 'function' ? (arg as (prev: Value) => Value)(get(this)) : arg | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export * from './atom' | ||
export * from './internals' | ||
export * from './store' | ||
export * from './typeUtils' | ||
export * from './useAtom' | ||
export * from './useAtomValue' | ||
export * from './useSetAtom' | ||
export * from './Provider' |
Oops, something went wrong.