UNPKG

recoil

Version:

Recoil - A state management library for React

219 lines (193 loc) 8.72 kB
/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * Return an atom whose state cannot vary independently but is derived from that * of other atoms. Whenever its dependency atoms change, it will re-evaluate * a function and pass along the result to any components or further selectors: * * const exampleSelector = selector({ * key: 'example', * get: ({get}) => { * const a = get(atomA); * const b = get(atomB); * return a + b; * }, * }); * * In this example, the value of exampleSelector will be the sum of atomA and atomB. * This sum will be updated whenever either atomA or atomB changes. The value * returned by the function will be deeply frozen. * * The function is only reevaluated if the dependencies change and the selector * has a component subscribed to it (either directly or indirectly via other * selectors). By default, function results are cached, so if the same values * of the dependencies are seen again, the cached value will be returned instead * of the function being reevaluated. The caching behavior can be overridden * by providing the `cacheImplementation` option; this can be used to discard * old values or to provide different equality semantics. * * If the provided function returns a Promise, it will cause the value of the * atom to become unavailable until the promise resolves. This means that any * components subscribed to the selector will suspend. If the promise is rejected, * any subscribed components will throw the rejecting error during rendering. * * You can provide the `set` option to allow writing to the selector. This * should be used sparingly; maintain a conceptual separation between independent * state and derived values. The `set` function receives a function to set * upstream RecoilValues which can accept a value or an updater function. * The updater function provides parameters with the old value of the RecoilValue * as well as a get() function to read other RecoilValues. * * const multiplierSelector = selector({ * key: 'multiplier', * get: ({get}) => get(atomA) * 100, * set: ({set, reset, get}, newValue) => set(atomA, newValue / 100), * }); * * @emails oncall+recoil * @flow strict-local * @format */ 'use strict'; import type { Loadable, LoadingLoadableType } from '../adt/Recoil_Loadable'; import type { CachePolicy } from '../caches/Recoil_CachePolicy'; import type { NodeCacheRoute, TreeCacheImplementation } from '../caches/Recoil_TreeCacheImplementationType'; import type { StateID } from '../core/Recoil_Keys'; import type { DefaultValue } from '../core/Recoil_Node'; import type { RecoilState, RecoilValue, RecoilValueReadOnly } from '../core/Recoil_RecoilValue'; import type { RetainedBy } from '../core/Recoil_RetainedBy'; import type { AtomWrites, NodeKey, Store, TreeState } from '../core/Recoil_State'; import type { RecoilCallbackInterface } from '../hooks/Recoil_useRecoilCallback'; import type { GetRecoilValue, ResetRecoilState, SetRecoilState } from './Recoil_callbackTypes'; const { loadableWithError, loadableWithPromise, loadableWithValue } = require('../adt/Recoil_Loadable'); const treeCacheFromPolicy = require('../caches/Recoil_treeCacheFromPolicy'); const { getNodeLoadable, peekNodeLoadable, setNodeValue } = require('../core/Recoil_FunctionalCore'); const { saveDependencyMapToStore } = require('../core/Recoil_Graph'); const { DEFAULT_VALUE, RecoilValueNotReady, getConfigDeletionHandler, getNode, registerNode } = require('../core/Recoil_Node'); const { isRecoilValue } = require('../core/Recoil_RecoilValue'); const { markRecoilValueModified } = require('../core/Recoil_RecoilValueInterface'); const { retainedByOptionWithDefault } = require('../core/Recoil_Retention'); const { recoilCallback } = require('../hooks/Recoil_useRecoilCallback'); const deepFreezeValue = require('recoil-shared/util/Recoil_deepFreezeValue'); const err = require('recoil-shared/util/Recoil_err'); const gkx = require('recoil-shared/util/Recoil_gkx'); const invariant = require('recoil-shared/util/Recoil_invariant'); const isPromise = require('recoil-shared/util/Recoil_isPromise'); const nullthrows = require('recoil-shared/util/Recoil_nullthrows'); const { startPerfBlock } = require('recoil-shared/util/Recoil_PerformanceTimings'); type SelectorCallbackInterface<T> = $ReadOnly<{ // TODO Technically this could be RecoilValueReadOnly, but trying to parameterize // it based on the selector type ran into problems which would lead to // dangerous error suppressions. node: RecoilState<T>, ...RecoilCallbackInterface, }>; export type GetCallback<T> = <Args: $ReadOnlyArray<mixed>, Return>(fn: (SelectorCallbackInterface<T>) => (...Args) => Return) => (...Args) => Return; type BaseSelectorOptions = $ReadOnly<{ key: string, dangerouslyAllowMutability?: boolean, retainedBy_UNSTABLE?: RetainedBy, cachePolicy_UNSTABLE?: CachePolicy, }>; export type ReadOnlySelectorOptions<T> = $ReadOnly<{ ...BaseSelectorOptions, get: ({ get: GetRecoilValue, getCallback: GetCallback<T>, }) => Promise<T> | RecoilValue<T> | T, }>; export type ReadWriteSelectorOptions<T> = $ReadOnly<{ ...ReadOnlySelectorOptions<T>, set: ({ set: SetRecoilState, get: GetRecoilValue, reset: ResetRecoilState, }, newValue: T | DefaultValue) => void, }>; export type SelectorOptions<T, P> = ReadOnlySelectorOptions<T, P> | ReadWriteSelectorOptions<T, P>; export type DepValues = Map<NodeKey, Loadable<mixed>>; declare class Canceled {} const CANCELED: Canceled = new Canceled(); /** * An ExecutionId is an arbitrary ID that lets us distinguish executions from * each other. This is necessary as we need a way of solving this problem: * "given 3 async executions, only update state for the 'latest' execution when * it finishes running regardless of when the other 2 finish". ExecutionIds * provide a convenient way of identifying executions so that we can track and * manage them over time. */ type ExecutionId = number; /** * ExecutionInfo is useful for managing async work and resolving race * conditions. It keeps track of the following: * * 1. The dep values found so far for the latest running execution. This is * useful for answering the question "given a new state, have any of the * async execution's discovered dep values changed?" * 2. The latest loadable, which holds the loadable of the latest execution. * This is important because we need to return this loadable when the * selector's result is requested and there is a pending async execution. We * are essentially caching the latest loading loadable without using the * actual selector cache so that we can avoid creating cache keys that use * partial dependencies (we never want to cache based on partial * dependencies). * 3. The latest execution ID, which is needed to know whether or not an async * execution is stale. At any point in time there may be any number of stale * executions running, but there is only one 'latest' execution, which * represents the execution that will make its way to the UI and make updates * to global state when it finishes. */ type ExecutionInfo<T> = { depValuesDiscoveredSoFarDuringAsyncWork: DepValues, latestLoadable: LoadingLoadableType<T>, latestExecutionId: ExecutionId, stateVersion: StateID, }; // An object to hold the current state for loading dependencies for a particular // execution of a selector. This is used for async selector handling to know // which dependency was pending or if a user-promise was thrown. An object is // used instead of just a variable with the loadingDepKey so that if the // selector is async we can still access the current state in a promise chain // by updating the object reference. type LoadingDepsState = { loadingDepKey: NodeKey | null, loadingDepPromise: Promise<mixed> | null, }; const dependencyStack = []; // for detecting circular dependencies. const waitingStores: Map<ExecutionId, Set<Store>> = new Map(); const getNewExecutionId: () => ExecutionId = (() => { let executionId = 0; return () => executionId++; })(); /* eslint-disable no-redeclare */ declare function selector<T>(options: ReadOnlySelectorOptions<T>): RecoilValueReadOnly<T>; declare function selector<T>(options: ReadWriteSelectorOptions<T>): RecoilState<T>; declare function selector<T>(options: ReadOnlySelectorOptions<T> | ReadWriteSelectorOptions<T>): RecoilValue<T>; /* eslint-enable no-redeclare */ module.exports = selector;