in packages/recoil/recoil_values/Recoil_atom.js [170:573]
function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
const {key, persistence_UNSTABLE: persistence} = options;
const retainedBy = retainedByOptionWithDefault(options.retainedBy_UNSTABLE);
let liveStoresCount = 0;
function unwrapPromise(promise: Promise<T>): Loadable<T> {
return loadableWithPromise(
promise
.then(value => {
defaultLoadable = loadableWithValue(value);
return value;
})
.catch(error => {
defaultLoadable = loadableWithError(error);
throw error;
}),
);
}
let defaultLoadable: Loadable<T> = isPromise(options.default)
? unwrapPromise(options.default)
: isLoadable(options.default)
? options.default.state === 'loading'
? unwrapPromise((options.default: LoadingLoadableType<T>).contents)
: options.default
: loadableWithValue(
options.default instanceof WrappedValue
? options.default.value
: options.default,
);
maybeFreezeValueOrPromise(defaultLoadable.contents);
let cachedAnswerForUnvalidatedValue: void | Loadable<T> = undefined;
// Cleanup handlers for this atom
// Rely on stable reference equality of the store to use it as a key per <RecoilRoot>
const cleanupEffectsByStore: Map<Store, Array<() => void>> = new Map();
function maybeFreezeValueOrPromise(valueOrPromise) {
if (__DEV__) {
if (options.dangerouslyAllowMutability !== true) {
if (isPromise(valueOrPromise)) {
return valueOrPromise.then(value => {
deepFreezeValue(value);
return value;
});
} else {
deepFreezeValue(valueOrPromise);
return valueOrPromise;
}
}
}
return valueOrPromise;
}
function wrapPendingPromise(
store: Store,
promise: Promise<T | DefaultValue>,
): Promise<T | DefaultValue> {
const wrappedPromise = promise
.then(value => {
const state = store.getState().nextTree ?? store.getState().currentTree;
if (state.atomValues.get(key)?.contents === wrappedPromise) {
setRecoilValue(store, node, value);
}
return value;
})
.catch(error => {
const state = store.getState().nextTree ?? store.getState().currentTree;
if (state.atomValues.get(key)?.contents === wrappedPromise) {
setRecoilValueLoadable(store, node, loadableWithError(error));
}
throw error;
});
return wrappedPromise;
}
function initAtom(
store: Store,
initState: TreeState,
trigger: Trigger,
): () => void {
liveStoresCount++;
const cleanupAtom = () => {
liveStoresCount--;
cleanupEffectsByStore.get(store)?.forEach(cleanup => cleanup());
cleanupEffectsByStore.delete(store);
};
store.getState().knownAtoms.add(key);
// Setup async defaults to notify subscribers when they resolve
if (defaultLoadable.state === 'loading') {
const notifyDefaultSubscribers = () => {
const state = store.getState().nextTree ?? store.getState().currentTree;
if (!state.atomValues.has(key)) {
markRecoilValueModified(store, node);
}
};
defaultLoadable.contents.finally(notifyDefaultSubscribers);
}
///////////////////
// Run Atom Effects
///////////////////
const effects = options.effects ?? options.effects_UNSTABLE;
if (effects != null) {
// This state is scoped by Store, since this is in the initAtom() closure
let duringInit = true;
let initValue: NewValue<T> = DEFAULT_VALUE;
let isInitError: boolean = false;
let pendingSetSelf: ?{
effect: AtomEffect<T>,
value: T | DefaultValue,
} = null;
function getLoadable<S>(recoilValue: RecoilValue<S>): Loadable<S> {
// Normally we can just get the current value of another atom.
// But for our own value we need to check if there is a pending
// initialized value or get the fallback default value.
if (duringInit && recoilValue.key === key) {
// Cast T to S
const retValue: NewValue<S> = (initValue: any); // flowlint-line unclear-type:off
return retValue instanceof DefaultValue
? (peekAtom(store, initState): any) // flowlint-line unclear-type:off
: isPromise(retValue)
? loadableWithPromise(
retValue.then((v: S | DefaultValue): S | Promise<S> =>
v instanceof DefaultValue
? // Cast T to S
(defaultLoadable: any).toPromise() // flowlint-line unclear-type:off
: v,
),
)
: loadableWithValue(retValue);
}
return getRecoilValueAsLoadable(store, recoilValue);
}
function getPromise<S>(recoilValue: RecoilValue<S>): Promise<S> {
return getLoadable(recoilValue).toPromise();
}
function getInfo_UNSTABLE<S>(
recoilValue: RecoilValue<S>,
): RecoilValueInfo<S> {
const info = peekNodeInfo(
store,
store.getState().nextTree ?? store.getState().currentTree,
recoilValue.key,
);
return duringInit &&
recoilValue.key === key &&
!(initValue instanceof DefaultValue)
? {...info, isSet: true, loadable: getLoadable(recoilValue)}
: info;
}
const setSelf =
(effect: AtomEffect<T>) => (valueOrUpdater: NewValueOrUpdater<T>) => {
if (duringInit) {
const currentLoadable = getLoadable(node);
const currentValue: T | DefaultValue =
currentLoadable.state === 'hasValue'
? currentLoadable.contents
: DEFAULT_VALUE;
initValue =
typeof valueOrUpdater === 'function'
? // cast to any because we can't restrict T from being a function without losing support for opaque types
(valueOrUpdater: any)(currentValue) // flowlint-line unclear-type:off
: valueOrUpdater;
if (isPromise(initValue)) {
initValue = initValue.then(value => {
// Avoid calling onSet() when setSelf() initializes with a Promise
pendingSetSelf = {effect, value};
return value;
});
}
} else {
if (isPromise(valueOrUpdater)) {
throw err('Setting atoms to async values is not implemented.');
}
if (typeof valueOrUpdater !== 'function') {
pendingSetSelf = {effect, value: valueOrUpdater};
}
setRecoilValue(
store,
node,
typeof valueOrUpdater === 'function'
? currentValue => {
const newValue =
// cast to any because we can't restrict T from being a function without losing support for opaque types
(valueOrUpdater: any)(currentValue); // flowlint-line unclear-type:off
pendingSetSelf = {effect, value: newValue};
return newValue;
}
: valueOrUpdater,
);
}
};
const resetSelf = effect => () => setSelf(effect)(DEFAULT_VALUE);
const onSet =
effect => (handler: (T, T | DefaultValue, boolean) => void) => {
const {release} = store.subscribeToTransactions(currentStore => {
// eslint-disable-next-line prefer-const
let {currentTree, previousTree} = currentStore.getState();
if (!previousTree) {
recoverableViolation(
'Transaction subscribers notified without a next tree being present -- this is a bug in Recoil',
'recoil',
);
previousTree = currentTree; // attempt to trundle on
}
const newLoadable =
currentTree.atomValues.get(key) ?? defaultLoadable;
if (newLoadable.state === 'hasValue') {
const newValue: T = newLoadable.contents;
const oldLoadable =
previousTree.atomValues.get(key) ?? defaultLoadable;
const oldValue: T | DefaultValue =
oldLoadable.state === 'hasValue'
? oldLoadable.contents
: DEFAULT_VALUE; // TODO This isn't actually valid, use as a placeholder for now.
// Ignore atom value changes that were set via setSelf() in the same effect.
// We will still properly call the handler if there was a subsequent
// set from something other than an atom effect which was batched
// with the `setSelf()` call. However, we may incorrectly ignore
// the handler if the subsequent batched call happens to set the
// atom to the exact same value as the `setSelf()`. But, in that
// case, it was kind of a noop, so the semantics are debatable..
if (
pendingSetSelf?.effect !== effect ||
pendingSetSelf?.value !== newValue
) {
handler(newValue, oldValue, !currentTree.atomValues.has(key));
} else if (pendingSetSelf?.effect === effect) {
pendingSetSelf = null;
}
}
}, key);
cleanupEffectsByStore.set(store, [
...(cleanupEffectsByStore.get(store) ?? []),
release,
]);
};
for (const effect of effects) {
try {
const cleanup = effect({
node,
storeID: store.storeID,
trigger,
setSelf: setSelf(effect),
resetSelf: resetSelf(effect),
onSet: onSet(effect),
getPromise,
getLoadable,
getInfo_UNSTABLE,
});
if (cleanup != null) {
cleanupEffectsByStore.set(store, [
...(cleanupEffectsByStore.get(store) ?? []),
cleanup,
]);
}
} catch (error) {
initValue = error;
isInitError = true;
}
}
duringInit = false;
// Mutate initial state in place since we know there are no other subscribers
// since we are the ones initializing on first use.
if (!(initValue instanceof DefaultValue)) {
const frozenInitValue = maybeFreezeValueOrPromise(initValue);
const initLoadable = isInitError
? loadableWithError(initValue)
: isPromise(frozenInitValue)
? loadableWithPromise(wrapPendingPromise(store, frozenInitValue))
: loadableWithValue(frozenInitValue);
initState.atomValues.set(key, initLoadable);
// If there is a pending transaction, then also mutate the next state tree.
// This could happen if the atom was first initialized in an action that
// also updated some other atom's state.
store.getState().nextTree?.atomValues.set(key, initLoadable);
}
}
return cleanupAtom;
}
function peekAtom(_store, state: TreeState): Loadable<T> {
return (
state.atomValues.get(key) ??
cachedAnswerForUnvalidatedValue ??
defaultLoadable
);
}
function getAtom(_store: Store, state: TreeState): Loadable<T> {
if (state.atomValues.has(key)) {
// Atom value is stored in state:
return nullthrows(state.atomValues.get(key));
} else if (state.nonvalidatedAtoms.has(key)) {
// Atom value is stored but needs validation before use.
// We might have already validated it and have a cached validated value:
if (cachedAnswerForUnvalidatedValue != null) {
return cachedAnswerForUnvalidatedValue;
}
if (persistence == null) {
expectationViolation(
`Tried to restore a persisted value for atom ${key} but it has no persistence settings.`,
);
return defaultLoadable;
}
const nonvalidatedValue = state.nonvalidatedAtoms.get(key);
const validatorResult: T | DefaultValue = persistence.validator(
nonvalidatedValue,
DEFAULT_VALUE,
);
const validatedValueLoadable =
validatorResult instanceof DefaultValue
? defaultLoadable
: loadableWithValue(validatorResult);
cachedAnswerForUnvalidatedValue = validatedValueLoadable;
return cachedAnswerForUnvalidatedValue;
} else {
return defaultLoadable;
}
}
function invalidateAtom() {
cachedAnswerForUnvalidatedValue = undefined;
}
function setAtom(
_store: Store,
state: TreeState,
newValue: T | DefaultValue,
): AtomWrites {
// Bail out if we're being set to the existing value, or if we're being
// reset but have no stored value (validated or unvalidated) to reset from:
if (state.atomValues.has(key)) {
const existing = nullthrows(state.atomValues.get(key));
if (existing.state === 'hasValue' && newValue === existing.contents) {
return new Map();
}
} else if (
!state.nonvalidatedAtoms.has(key) &&
newValue instanceof DefaultValue
) {
return new Map();
}
maybeFreezeValueOrPromise(newValue);
cachedAnswerForUnvalidatedValue = undefined; // can be released now if it was previously in use
return new Map().set(key, loadableWithValue(newValue));
}
function shouldDeleteConfigOnReleaseAtom() {
return getConfigDeletionHandler(key) !== undefined && liveStoresCount <= 0;
}
const node = registerNode(
({
key,
nodeType: 'atom',
peek: peekAtom,
get: getAtom,
set: setAtom,
init: initAtom,
invalidate: invalidateAtom,
shouldDeleteConfigOnRelease: shouldDeleteConfigOnReleaseAtom,
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
persistence_UNSTABLE: options.persistence_UNSTABLE
? {
type: options.persistence_UNSTABLE.type,
backButton: options.persistence_UNSTABLE.backButton,
}
: undefined,
shouldRestoreFromSnapshots: true,
retainedBy,
}: ReadWriteNodeOptions<T>),
);
return node;
}