diff --git a/.size-snapshot.json b/.size-snapshot.json index b79302fbf1..37a07c1641 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "index.js": { - "bundled": 22130, - "minified": 8884, - "gzipped": 3283, + "bundled": 19763, + "minified": 8215, + "gzipped": 3043, "treeshaked": { "rollup": { "code": 14, @@ -14,9 +14,9 @@ } }, "index.mjs": { - "bundled": 22130, - "minified": 8884, - "gzipped": 3283, + "bundled": 19763, + "minified": 8215, + "gzipped": 3043, "treeshaked": { "rollup": { "code": 14, @@ -56,9 +56,9 @@ } }, "devtools.js": { - "bundled": 20426, - "minified": 8380, - "gzipped": 3171, + "bundled": 20062, + "minified": 8222, + "gzipped": 3114, "treeshaked": { "rollup": { "code": 28, @@ -70,9 +70,9 @@ } }, "devtools.mjs": { - "bundled": 20426, - "minified": 8380, - "gzipped": 3171, + "bundled": 20062, + "minified": 8222, + "gzipped": 3114, "treeshaked": { "rollup": { "code": 28, diff --git a/package.json b/package.json index a7fe6eb65b..daa02ec890 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,7 @@ "tests/**/*.{js,ts,tsx}" ] }, + "dependencies": {}, "devDependencies": { "@babel/core": "^7.15.0", "@babel/plugin-transform-react-jsx": "^7.14.9", diff --git a/src/core/Provider.ts b/src/core/Provider.ts index e7552607f4..88b1166fec 100644 --- a/src/core/Provider.ts +++ b/src/core/Provider.ts @@ -1,4 +1,10 @@ -import { createElement, useCallback, useDebugValue, useRef } from 'react' +import { + createElement, + useDebugValue, + useEffect, + useRef, + useState, +} from 'react' import type { PropsWithChildren } from 'react' import type { Atom, Scope } from './atom' import { @@ -10,7 +16,6 @@ import { import type { ScopeContainerForDevelopment } from './contexts' import { DEV_GET_ATOM_STATE, DEV_GET_MOUNTED } from './store' import type { AtomState, Store } from './store' -import { useMutableSource } from './useMutableSource' export const Provider = ({ initialValues, @@ -40,9 +45,7 @@ export const Provider = ({ return createElement( ScopeContainerContext.Provider, { - value: scopeContainerRef.current as ReturnType< - typeof createScopeContainer - >, + value: scopeContainerRef.current, }, children ) @@ -75,11 +78,14 @@ const stateToPrintable = ([store, atoms]: [Store, Atom[]]) => // We keep a reference to the atoms in Provider's registeredAtoms in dev mode, // so atoms aren't garbage collected by the WeakMap of mounted atoms const useDebugState = (scopeContainer: ScopeContainerForDevelopment) => { - const [store, , devMutableSource, devSubscribe] = scopeContainer - const atoms = useMutableSource( - devMutableSource, - useCallback((devContainer) => devContainer.atoms, []), - devSubscribe - ) + const [store, devStore] = scopeContainer + const [atoms, setAtoms] = useState(devStore.atoms) + useEffect(() => { + // HACK creating a new reference for useDebugValue to update + const callback = () => setAtoms([...devStore.atoms]) + const unsubscribe = devStore.subscribe(callback) + callback() + return unsubscribe + }, [devStore]) useDebugValue([store, atoms], stateToPrintable) } diff --git a/src/core/contexts.ts b/src/core/contexts.ts index cada3fc217..4681e6460f 100644 --- a/src/core/contexts.ts +++ b/src/core/contexts.ts @@ -1,50 +1,46 @@ import { createContext } from 'react' import type { Context } from 'react' import type { Atom, Scope } from './atom' -import { GET_VERSION, createStore } from './store' -import { createMutableSource } from './useMutableSource' +import { createStore } from './store' const createScopeContainerForProduction = ( initialValues?: Iterable, unknown]> ) => { const store = createStore(initialValues) - const mutableSource = createMutableSource(store, store[GET_VERSION]) - return [store, mutableSource] as const + return [store] as const } const createScopeContainerForDevelopment = ( initialValues?: Iterable, unknown]> ) => { - let devVersion = 0 - const devListeners = new Set<() => void>() - const devContainer = { + const devStore = { + listeners: new Set<() => void>(), + subscribe: (callback: () => void) => { + devStore.listeners.add(callback) + return () => { + devStore.listeners.delete(callback) + } + }, atoms: Array.from(initialValues ?? []).map(([a]) => a), } const stateListener = (updatedAtom: Atom, isNewAtom: boolean) => { - ++devVersion if (isNewAtom) { // FIXME memory leak // we should probably remove unmounted atoms eventually - devContainer.atoms = [...devContainer.atoms, updatedAtom] + devStore.atoms = [...devStore.atoms, updatedAtom] } Promise.resolve().then(() => { - devListeners.forEach((listener) => listener()) + devStore.listeners.forEach((listener) => listener()) }) } const store = createStore(initialValues, stateListener) - const mutableSource = createMutableSource(store, store[GET_VERSION]) - const devMutableSource = createMutableSource(devContainer, () => devVersion) - const devSubscribe = (_: unknown, callback: () => void) => { - devListeners.add(callback) - return () => devListeners.delete(callback) - } - return [store, mutableSource, devMutableSource, devSubscribe] as const + return [store, devStore] as const } export const isDevScopeContainer = ( scopeContainer: ScopeContainer ): scopeContainer is ScopeContainerForDevelopment => { - return scopeContainer.length > 2 + return scopeContainer.length > 1 } type ScopeContainerForProduction = ReturnType< diff --git a/src/core/store.ts b/src/core/store.ts index a9075bc5bc..f927b2cb65 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -66,7 +66,6 @@ type Mounted = { type StateListener = (updatedAtom: AnyAtom, isNewAtom: boolean) => void // store methods -export const GET_VERSION = 'v' export const READ_ATOM = 'r' export const WRITE_ATOM = 'w' export const FLUSH_PENDING = 'f' @@ -79,7 +78,6 @@ export const createStore = ( initialValues?: Iterable, stateListener?: StateListener ) => { - let version = 0 const atomStateMap = new WeakMap() const mountedMap = new WeakMap() const pendingMap = new Map() @@ -142,6 +140,9 @@ export const createStore = ( if (!('v' in atomState) || !Object.is(atomState.v, value)) { atomState.v = value ++atomState.r // increment revision + if (atomState.d.has(atom)) { + atomState.d.set(atom, atomState.r) + } } commitAtomState(atom, atomState, dependencies && prevDependencies) } @@ -569,7 +570,6 @@ export const createStore = ( if (stateListener) { stateListener(atom, isNewAtom) } - ++version if (!pendingMap.has(atom)) { pendingMap.set(atom, prevDependencies) } @@ -619,7 +619,6 @@ export const createStore = ( if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { return { - [GET_VERSION]: () => version, [READ_ATOM]: readAtom, [WRITE_ATOM]: writeAtom, [FLUSH_PENDING]: flushPending, @@ -630,7 +629,6 @@ export const createStore = ( } } return { - [GET_VERSION]: () => version, [READ_ATOM]: readAtom, [WRITE_ATOM]: writeAtom, [FLUSH_PENDING]: flushPending, diff --git a/src/core/useAtom.ts b/src/core/useAtom.ts index 76cfeec748..b95625f9bb 100644 --- a/src/core/useAtom.ts +++ b/src/core/useAtom.ts @@ -1,9 +1,13 @@ -import { useCallback, useContext, useDebugValue, useEffect } from 'react' +import { + useCallback, + useContext, + useDebugValue, + useEffect, + useReducer, +} from 'react' import type { Atom, Scope, SetAtom, WritableAtom } from './atom' import { getScopeContext } from './contexts' import { FLUSH_PENDING, READ_ATOM, SUBSCRIBE_ATOM, WRITE_ATOM } from './store' -import type { Store } from './store' -import { useMutableSource } from './useMutableSource' const isWritable = ( atom: Atom | WritableAtom @@ -41,32 +45,6 @@ export function useAtom( atom: Atom | WritableAtom, scope?: Scope ) { - const getAtomValue = useCallback( - (store: Store) => { - const atomState = store[READ_ATOM](atom) - if (atomState.e) { - throw atomState.e // read error - } - if (atomState.p) { - throw atomState.p // read promise - } - if (atomState.w) { - throw atomState.w // write promise - } - if ('v' in atomState) { - return atomState.v as Value - } - throw new Error('no atom value') - }, - [atom] - ) - - const subscribe = useCallback( - (store: Store, callback: () => void) => - store[SUBSCRIBE_ATOM](atom, callback), - [atom] - ) - if ('scope' in atom) { console.warn( 'atom.scope is deprecated. Please do useAtom(atom, scope) instead.' @@ -75,8 +53,33 @@ export function useAtom( } const ScopeContext = getScopeContext(scope) - const [store, mutableSource] = useContext(ScopeContext) - const value = useMutableSource(mutableSource, getAtomValue, subscribe) + const [store] = useContext(ScopeContext) + + const getAtomValue = useCallback(() => { + const atomState = store[READ_ATOM](atom) + if (atomState.e) { + throw atomState.e // read error + } + if (atomState.p) { + throw atomState.p // read promise + } + if (atomState.w) { + throw atomState.w // write promise + } + if ('v' in atomState) { + return atomState.v as Value + } + throw new Error('no atom value') + }, [store, atom]) + + const [value, forceUpdate] = useReducer(getAtomValue, undefined, getAtomValue) + + useEffect(() => { + const unsubscribe = store[SUBSCRIBE_ATOM](atom, forceUpdate) + forceUpdate() + return unsubscribe + }, [store, atom]) + useEffect(() => { store[FLUSH_PENDING]() }) diff --git a/src/core/useMutableSource.ts b/src/core/useMutableSource.ts deleted file mode 100644 index 2b1599e21e..0000000000 --- a/src/core/useMutableSource.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* -export { - unstable_createMutableSource as createMutableSource, - unstable_useMutableSource as useMutableSource, -} from 'react' -*/ - -// useMutableSource emulation almost equivalent to useSubscription - -import { useEffect, useRef, useState } from 'react' - -const TARGET = '_uMS_T' -const GET_VERSION = '_uMS_V' - -type MutableSource = { - [TARGET]: T - [GET_VERSION]: (target: T) => V -} - -export const createMutableSource = ( - target: T, - getVersion: (target: T) => V -): MutableSource => ({ - [TARGET]: target, - [GET_VERSION]: getVersion, -}) - -export const useMutableSource = ( - source: MutableSource, - getSnapshot: (target: T) => S, - subscribe: (target: T, callback: () => void) => () => void -) => { - const lastVersion = useRef() - const currentVersion = source[GET_VERSION](source[TARGET]) - const [state, setState] = useState( - () => - [ - /* [0] */ source, - /* [1] */ getSnapshot, - /* [2] */ subscribe, - /* [3] */ currentVersion, - /* [4] */ getSnapshot(source[TARGET]), - ] as const - ) - let currentSnapshot = state[4] - if ( - state[0] !== source || - state[1] !== getSnapshot || - state[2] !== subscribe - ) { - currentSnapshot = getSnapshot(source[TARGET]) - setState([ - /* [0] */ source, - /* [1] */ getSnapshot, - /* [2] */ subscribe, - /* [3] */ currentVersion, - /* [4] */ currentSnapshot, - ]) - } else if ( - currentVersion !== state[3] && - currentVersion !== lastVersion.current - ) { - currentSnapshot = getSnapshot(source[TARGET]) - if (!Object.is(currentSnapshot, state[4])) { - setState([ - /* [0] */ source, - /* [1] */ getSnapshot, - /* [2] */ subscribe, - /* [3] */ currentVersion, - /* [4] */ currentSnapshot, - ]) - } - } - useEffect(() => { - let didUnsubscribe = false - const checkForUpdates = () => { - if (didUnsubscribe) { - return - } - try { - const nextSnapshot = getSnapshot(source[TARGET]) - const nextVersion = source[GET_VERSION](source[TARGET]) - lastVersion.current = nextVersion - setState((prev) => { - if ( - prev[0] !== source || - prev[1] !== getSnapshot || - prev[2] !== subscribe - ) { - return prev - } - if (Object.is(prev[4], nextSnapshot)) { - return prev - } - return [ - /* [0] */ prev[0], - /* [1] */ prev[1], - /* [2] */ prev[2], - /* [3] */ nextVersion, - /* [4] */ nextSnapshot, - ] - }) - } catch (e) { - // schedule update - setState((prev) => [...prev]) - } - } - const unsubscribe = subscribe(source[TARGET], checkForUpdates) - checkForUpdates() - return () => { - didUnsubscribe = true - unsubscribe() - } - }, [source, getSnapshot, subscribe]) - return currentSnapshot -} diff --git a/src/devtools/useAtomsSnapshot.ts b/src/devtools/useAtomsSnapshot.ts index cad49e1305..587ae19cf0 100644 --- a/src/devtools/useAtomsSnapshot.ts +++ b/src/devtools/useAtomsSnapshot.ts @@ -1,8 +1,5 @@ -import { useCallback, useContext } from 'react' -import { - SECRET_INTERNAL_getScopeContext as getScopeContext, - SECRET_INTERNAL_useMutableSource as useMutableSource, -} from 'jotai' +import { useContext, useEffect, useState } from 'react' +import { SECRET_INTERNAL_getScopeContext as getScopeContext } from 'jotai' import type { Atom, Scope } from '../core/atom' // NOTE importing from '../core/contexts' is across bundles and actually copying code import { isDevScopeContainer } from '../core/contexts' @@ -19,20 +16,25 @@ export function useAtomsSnapshot(scope?: Scope): AtomsSnapshot { throw Error('useAtomsSnapshot can only be used in dev mode.') } - const [store, , devMutableSource, devSubscribe] = scopeContainer + const [store, devStore] = scopeContainer - const atoms = useMutableSource( - devMutableSource, - // FIXME HACK creating new reference to force re-render - useCallback((devContainer) => [...devContainer.atoms], []), - devSubscribe - ) + const [atomsSnapshot, setAtomsSnapshot] = useState(new Map()) + useEffect(() => { + const callback = () => { + const { atoms } = devStore + const atomToAtomValueTuples = atoms + .filter((atom) => !!store[DEV_GET_MOUNTED]?.(atom)) + .map<[Atom, unknown]>((atom) => { + const atomState = + store[DEV_GET_ATOM_STATE]?.(atom) ?? ({} as AtomState) + return [atom, atomState.v] + }) + setAtomsSnapshot(new Map(atomToAtomValueTuples)) + } + const unsubscribe = devStore.subscribe(callback) + callback() + return unsubscribe + }, [store, devStore]) - const atomToAtomValueTuples = atoms - .filter((atom) => !!store[DEV_GET_MOUNTED]?.(atom)) - .map<[Atom, unknown]>((atom) => { - const atomState = store[DEV_GET_ATOM_STATE]?.(atom) ?? ({} as AtomState) - return [atom, atomState.v] - }) - return new Map(atomToAtomValueTuples) + return atomsSnapshot } diff --git a/src/index.ts b/src/index.ts index 49dd947e1d..33a51f704c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,9 +15,3 @@ export type { * It can change without notice. Do not use it in application code. */ export { getScopeContext as SECRET_INTERNAL_getScopeContext } from './core/contexts' - -/** - * This is exported for internal use only. - * It can change without notice. Do not use it in application code. - */ -export { useMutableSource as SECRET_INTERNAL_useMutableSource } from './core/useMutableSource'