in packages/recoil/recoil_values/Recoil_atom.js [251:471]
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;
}