function baseAtom()

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;
}