jotai
Version:
👻 Next gen state management that will spook you
751 lines (603 loc) • 19.7 kB
JavaScript
import React, { useRef, useState, useCallback, useEffect, useMemo, createElement, useDebugValue } from 'react';
import { createContext, useContextUpdate, useContext, useContextSelector, useBridgeValue, BridgeProvider } from 'use-context-selector';
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
const hasInitialValue = atom => 'init' in atom;
const createState = initialValues => {
const state = {
a: new WeakMap(),
m: new Map(),
w: new Map()
};
if (initialValues) {
for (const [atom, value] of initialValues) {
const atomState = {
v: value,
r: 0,
d: new Map()
};
if (typeof process === 'object' && process.env.NODE_ENV !== 'production') {
Object.freeze(atomState);
}
state.a.set(atom, atomState);
}
}
return state;
};
const getAtomState = (state, atom) => state.w.get(atom) || state.a.get(atom);
const copyWip = (state, copyingState) => _extends({}, state, {
w: new Map([...state.w, ...copyingState.w])
});
const wipAtomState = (state, atom) => {
let atomState = getAtomState(state, atom);
if (atomState) {
atomState = _extends({}, atomState); // copy
} else {
atomState = {
r: 0,
d: new Map()
};
if (hasInitialValue(atom)) {
atomState.v = atom.init;
}
}
const nextState = _extends({}, state, {
w: new Map(state.w).set(atom, atomState)
});
return [atomState, nextState];
};
const replaceDependencies = (state, atomState, dependencies) => {
if (dependencies) {
atomState.d = new Map(Array.from(dependencies).map(a => {
var _getAtomState$r, _getAtomState;
return [a, (_getAtomState$r = (_getAtomState = getAtomState(state, a)) == null ? void 0 : _getAtomState.r) != null ? _getAtomState$r : 0];
}));
}
};
const setAtomValue = (state, atom, value, dependencies, promise) => {
const [atomState, nextState] = wipAtomState(state, atom);
if (promise && promise !== atomState.rp) {
return state;
}
delete atomState.re;
delete atomState.rp;
if (!('v' in atomState) || !Object.is(atomState.v, value)) {
atomState.v = value;
atomState.r++;
}
replaceDependencies(nextState, atomState, dependencies);
return nextState;
};
const setAtomReadError = (state, atom, error, dependencies, promise) => {
const [atomState, nextState] = wipAtomState(state, atom);
if (promise && promise !== atomState.rp) {
return state;
}
delete atomState.rp;
atomState.re = error;
replaceDependencies(nextState, atomState, dependencies);
return nextState;
};
const setAtomReadPromise = (state, atom, promise, dependencies) => {
const [atomState, nextState] = wipAtomState(state, atom);
atomState.rp = promise;
replaceDependencies(nextState, atomState, dependencies);
return nextState;
};
const setAtomWritePromise = (state, atom, promise) => {
const [atomState, nextState] = wipAtomState(state, atom);
if (promise) {
atomState.wp = promise;
} else {
delete atomState.wp;
}
return nextState;
};
const readAtomState = (state, updateState, atom, force) => {
if (!force) {
const atomState = getAtomState(state, atom);
if (atomState && Array.from(atomState.d.entries()).every(([a, r]) => {
const aState = getAtomState(state, a);
return aState && !aState.re && !aState.rp && aState.r === r;
})) {
return [atomState, state];
}
}
let asyncState = _extends({}, state, {
w: new Map()
}); // empty wip
let isSync = true;
let nextState = state;
let error;
let promise;
let value;
const dependencies = new Set();
try {
const promiseOrValue = atom.read(a => {
dependencies.add(a);
if (a !== atom) {
let aState;
if (isSync) {
;
[aState, nextState] = readAtomState(nextState, updateState, a);
} else {
;
[aState, asyncState] = readAtomState(asyncState, updateState, a);
}
if (aState.re) {
throw aState.re; // read error
}
if (aState.rp) {
throw aState.rp; // read promise
}
return aState.v; // value
} // a === atom
const aState = getAtomState(nextState, a);
if (aState) {
if (aState.rp) {
throw aState.rp; // read promise
}
return aState.v; // value
}
if (hasInitialValue(a)) {
return a.init;
}
throw new Error('no atom init');
});
if (promiseOrValue instanceof Promise) {
promise = promiseOrValue.then(value => {
updateState(prev => setAtomValue(copyWip(prev, asyncState), atom, value, dependencies, promise));
}).catch(e => {
updateState(prev => setAtomReadError(copyWip(prev, asyncState), atom, e instanceof Error ? e : new Error(e), dependencies, promise));
});
} else {
value = promiseOrValue;
}
} catch (errorOrPromise) {
if (errorOrPromise instanceof Promise) {
promise = errorOrPromise.then(() => {
updateState(prev => {
const [, nextNextState] = readAtomState(prev, updateState, atom, true);
if (nextNextState.w.size) {
return nextNextState;
}
return prev;
});
});
} else if (errorOrPromise instanceof Error) {
error = errorOrPromise;
} else {
error = new Error(errorOrPromise);
}
}
if (error) {
nextState = setAtomReadError(nextState, atom, error, dependencies);
} else if (promise) {
nextState = setAtomReadPromise(nextState, atom, promise, dependencies);
} else {
nextState = setAtomValue(nextState, atom, value, dependencies);
}
isSync = false;
return [getAtomState(nextState, atom), nextState];
};
const readAtom = (state, updateState, readingAtom) => {
const [atomState, nextState] = readAtomState(state, updateState, readingAtom); // merge back wip
nextState.w.forEach((atomState, atom) => {
state.w.set(atom, atomState);
});
return atomState;
};
const addAtom = (state, updateState, addingAtom, useId) => {
const mounted = state.m.get(addingAtom);
if (mounted) {
const [dependents] = mounted;
dependents.add(useId);
} else {
mountAtom(state, updateState, addingAtom, useId);
}
}; // XXX doesn't work with mutally dependent atoms
const canUnmountAtom = (atom, dependents) => !dependents.size || dependents.size === 1 && dependents.has(atom);
const delAtom = (state, deletingAtom, useId) => {
const mounted = state.m.get(deletingAtom);
if (mounted) {
const [dependents] = mounted;
dependents.delete(useId);
if (canUnmountAtom(deletingAtom, dependents)) {
unmountAtom(state, deletingAtom);
}
}
};
const getDependents = (state, atom) => {
const mounted = state.m.get(atom);
const dependents = new Set(mounted == null ? void 0 : mounted[0]); // collecting from wip
state.w.forEach((aState, a) => {
if (aState.d.has(atom)) {
dependents.add(a);
}
});
return dependents;
};
const updateDependentsState = (state, updateState, atom, prevAtomState) => {
var _getAtomState2;
if (!prevAtomState || prevAtomState.r === ((_getAtomState2 = getAtomState(state, atom)) == null ? void 0 : _getAtomState2.r)) {
return state; // bail out
}
const dependents = getDependents(state, atom);
let nextState = state;
dependents.forEach(dependent => {
if (dependent === atom || typeof dependent === 'symbol') {
return;
}
const dependentState = getAtomState(nextState, dependent);
const [nextDependentState, nextNextState] = readAtomState(nextState, updateState, dependent, true);
const promise = nextDependentState.rp;
if (promise) {
promise.then(() => {
updateState(prev => updateDependentsState(prev, updateState, dependent, dependentState));
});
nextState = nextNextState;
} else {
nextState = updateDependentsState(nextNextState, updateState, dependent, dependentState);
}
});
return nextState;
};
const writeAtomState = (state, updateState, atom, update, pendingPromises) => {
const atomState = getAtomState(state, atom);
if (atomState && atomState.wp) {
const promise = atomState.wp.then(() => {
updateState(prev => writeAtomState(prev, updateState, atom, update));
});
if (pendingPromises) {
pendingPromises.push(promise);
}
return state;
}
let nextState = state;
let isSync = true;
try {
const promiseOrVoid = atom.write(a => {
const aState = getAtomState(nextState, a);
if (!aState) {
if (hasInitialValue(a)) {
return a.init;
}
if (typeof process === 'object' && process.env.NODE_ENV !== 'production') {
console.warn('Unable to read an atom without initial value in write function. Please useAtom in advance.', a);
}
throw new Error('uninitialized atom');
}
if (aState.rp && typeof process === 'object' && process.env.NODE_ENV !== 'production') {
// TODO will try to detect this
console.warn('Reading pending atom state in write operation. We need to detect this and fallback. Please file an issue with repro.', a);
}
return aState.v;
}, (a, v) => {
if (a === atom) {
const aState = getAtomState(nextState, a);
if (isSync) {
nextState = updateDependentsState(setAtomValue(nextState, a, v), updateState, a, aState);
} else {
updateState(prev => updateDependentsState(setAtomValue(prev, a, v), updateState, a, aState));
}
} else {
if (isSync) {
nextState = writeAtomState(nextState, updateState, a, v);
} else {
updateState(prev => writeAtomState(prev, updateState, a, v));
}
}
}, update);
if (promiseOrVoid instanceof Promise) {
if (pendingPromises) {
pendingPromises.push(promiseOrVoid);
}
nextState = setAtomWritePromise(nextState, atom, promiseOrVoid.then(() => {
updateState(prev => setAtomWritePromise(prev, atom));
}));
}
} catch (e) {
if (pendingPromises && pendingPromises.length) {
pendingPromises.push(new Promise((_resolve, reject) => {
reject(e);
}));
} else {
throw e;
}
}
isSync = false;
return nextState;
};
const writeAtom = (updateState, writingAtom, update) => {
const pendingPromises = [];
updateState(prev => {
const nextState = writeAtomState(prev, updateState, writingAtom, update, pendingPromises);
return nextState;
});
if (pendingPromises.length) {
return new Promise((resolve, reject) => {
const loop = () => {
const len = pendingPromises.length;
if (len === 0) {
resolve();
} else {
Promise.all(pendingPromises).then(() => {
pendingPromises.splice(0, len);
loop();
}).catch(reject);
}
};
loop();
});
}
};
const isActuallyWritableAtom = atom => !!atom.write;
const mountAtom = (state, updateState, atom, initialDependent) => {
// mount dependencies beforehand
const atomState = getAtomState(state, atom);
if (atomState) {
atomState.d.forEach((_, a) => {
if (a !== atom) {
// check if not mounted
if (!state.m.has(a)) {
mountAtom(state, updateState, a, atom);
}
}
});
} else if (typeof process === 'object' && process.env.NODE_ENV !== 'production') {
console.warn('[Bug] could not find atom state to mount', atom);
} // mount self
let onUmount;
if (isActuallyWritableAtom(atom) && atom.onMount) {
const setAtom = update => writeAtom(updateState, atom, update);
onUmount = atom.onMount(setAtom);
}
state.m.set(atom, [new Set([initialDependent]), onUmount]);
};
const unmountAtom = (state, atom) => {
var _state$m$get;
// unmount self
const onUnmount = (_state$m$get = state.m.get(atom)) == null ? void 0 : _state$m$get[1];
if (onUnmount) {
onUnmount();
}
state.m.delete(atom); // unmount dependencies afterward
const atomState = getAtomState(state, atom);
if (atomState) {
if (atomState.rp && typeof process === 'object' && process.env.NODE_ENV !== 'production') {
console.warn('[Bug] deleting atomState with read promise', atom);
}
atomState.d.forEach((_, a) => {
if (a !== atom) {
var _state$m$get2;
const dependents = (_state$m$get2 = state.m.get(a)) == null ? void 0 : _state$m$get2[0];
if (dependents) {
dependents.delete(atom);
if (canUnmountAtom(a, dependents)) {
unmountAtom(state, a);
}
}
}
});
} else if (typeof process === 'object' && process.env.NODE_ENV !== 'production') {
console.warn('[Bug] could not find atom state to unmount', atom);
}
};
const commitState = (state, updateState) => {
if (state.w.size) {
// apply wip to MountedMap
state.w.forEach((atomState, atom) => {
var _state$a$get;
const prevDependencies = (_state$a$get = state.a.get(atom)) == null ? void 0 : _state$a$get.d;
if (prevDependencies === atomState.d) {
return;
}
const dependencies = new Set(atomState.d.keys());
if (prevDependencies) {
prevDependencies.forEach((_, a) => {
const mounted = state.m.get(a);
if (dependencies.has(a)) {
// not changed
dependencies.delete(a);
} else if (mounted) {
const [dependents] = mounted;
dependents.delete(atom);
if (canUnmountAtom(a, dependents)) {
unmountAtom(state, a);
}
} else if (typeof process === 'object' && process.env.NODE_ENV !== 'production') {
console.warn('[Bug] a dependency is not mounted', a);
}
});
}
dependencies.forEach(a => {
const mounted = state.m.get(a);
if (mounted) {
const [dependents] = mounted;
dependents.add(atom);
} else {
mountAtom(state, updateState, a, atom);
}
});
}); // copy wip to AtomStateMap
state.w.forEach((atomState, atom) => {
if (typeof process === 'object' && process.env.NODE_ENV !== 'production') {
Object.freeze(atomState);
}
state.a.set(atom, atomState);
}); // empty wip
state.w.clear();
}
};
const ContextsMap = new Map();
const getContexts = scope => {
if (!ContextsMap.has(scope)) {
ContextsMap.set(scope, [createContext(null), createContext(null)]);
}
return ContextsMap.get(scope);
};
const isReactExperimental = !!(typeof process === 'object' && process.env.IS_REACT_EXPERIMENTAL) || !!React.unstable_useMutableSource;
const defaultContextUpdate = f => f();
const InnerProvider = ({
r,
c,
children
}) => {
const contextUpdate = useContextUpdate(c);
if (isReactExperimental && r.current === defaultContextUpdate) {
r.current = f => contextUpdate(f);
}
return children != null ? children : null;
};
const Provider = ({
initialValues,
scope,
children
}) => {
const contextUpdateRef = useRef(defaultContextUpdate);
const [state, setState] = useState(() => createState(initialValues));
const lastStateRef = useRef(state);
const updateState = useCallback(updater => {
lastStateRef.current = updater(lastStateRef.current);
contextUpdateRef.current(() => {
setState(lastStateRef.current);
});
}, []);
useEffect(() => {
commitState(state, updateState);
lastStateRef.current = state;
});
const actions = useMemo(() => ({
add: (atom, id) => {
addAtom(lastStateRef.current, updateState, atom, id);
},
del: (atom, id) => {
delAtom(lastStateRef.current, atom, id);
},
read: (state, atom) => readAtom(state, updateState, atom),
write: (atom, update) => writeAtom(updateState, atom, update)
}), [updateState]);
if (typeof process === 'object' && process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line react-hooks/rules-of-hooks
useDebugState(state);
}
const [ActionsContext, StateContext] = getContexts(scope);
return createElement(ActionsContext.Provider, {
value: actions
}, createElement(StateContext.Provider, {
value: state
}, createElement(InnerProvider, {
r: contextUpdateRef,
c: StateContext
}, children)));
};
const atomToPrintable = atom => atom.debugLabel || atom.toString();
const isAtom = x => typeof x !== 'symbol';
const stateToPrintable = state => Object.fromEntries(Array.from(state.m.entries()).map(([atom, [dependents]]) => {
const atomState = state.a.get(atom) || {};
return [atomToPrintable(atom), {
value: atomState.re || atomState.rp || atomState.wp || atomState.v,
dependents: Array.from(dependents).filter(isAtom).map(atomToPrintable)
}];
}));
const useDebugState = state => {
useDebugValue(state, stateToPrintable);
};
let keyCount = 0; // global key count for all atoms
function atom(read, write) {
const key = `atom${++keyCount}`;
const config = {
toString: () => key
};
if (typeof read === 'function') {
config.read = read;
} else {
config.init = read;
config.read = get => get(config);
config.write = (get, set, update) => {
set(config, typeof update === 'function' ? update(get(config)) : update);
};
}
if (write) {
config.write = write;
}
return config;
}
function assertContextValue(x, scope) {
if (!x) {
throw new Error(`Please use <Provider${scope ? ` scope=${String(scope)}` : ''}>`);
}
}
const isWritable = atom => !!atom.write;
function useAtom(atom) {
const [ActionsContext, StateContext] = getContexts(atom.scope);
const actions = useContext(ActionsContext);
assertContextValue(actions, atom.scope);
const value = useContextSelector(StateContext, useCallback(state => {
assertContextValue(state);
const atomState = actions.read(state, atom);
if (atomState.re) {
throw atomState.re; // read error
}
if (atomState.rp) {
throw atomState.rp; // read promise
}
if (atomState.wp) {
throw atomState.wp; // write promise
}
if ('v' in atomState) {
return atomState.v;
}
throw new Error('no atom value');
}, [atom, actions]));
useEffect(() => {
const id = Symbol();
actions.add(atom, id);
return () => {
actions.del(atom, id);
};
}, [actions, atom]);
const setAtom = useCallback(update => {
if (isWritable(atom)) {
return actions.write(atom, update);
} else {
throw new Error('not writable atom');
}
}, [atom, actions]);
useDebugValue(value);
return [value, setAtom];
}
const useBridge = scope => {
const [ActionsContext, StateContext] = getContexts(scope);
const actions = useBridgeValue(ActionsContext);
const state = useBridgeValue(StateContext);
return useMemo(() => [actions, state], [actions, state]);
};
const Bridge = ({
value,
scope,
children
}) => {
const [actions, state] = value;
const [ActionsContext, StateContext] = getContexts(scope);
return createElement(BridgeProvider, {
context: ActionsContext,
value: actions
}, createElement(BridgeProvider, {
context: StateContext,
value: state
}, children));
};
export { Bridge, Provider, atom, useAtom, useBridge };