UNPKG

react-native-onyx

Version:

State management for React Native

258 lines (257 loc) 16.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fast_equals_1 = require("fast-equals"); const react_1 = require("react"); const OnyxCache_1 = __importStar(require("./OnyxCache")); const OnyxConnectionManager_1 = __importDefault(require("./OnyxConnectionManager")); const OnyxUtils_1 = __importDefault(require("./OnyxUtils")); const GlobalSettings = __importStar(require("./GlobalSettings")); const useLiveRef_1 = __importDefault(require("./useLiveRef")); const usePrevious_1 = __importDefault(require("./usePrevious")); const metrics_1 = __importDefault(require("./metrics")); const Logger = __importStar(require("./Logger")); /** * Gets the cached value from the Onyx cache. If the key is a collection key, it will return all the values in the collection. * It is a fork of `tryGetCachedValue` from `OnyxUtils` caused by different selector logic in `useOnyx`. It should be unified in the future, when `withOnyx` is removed. */ function tryGetCachedValue(key) { if (!OnyxUtils_1.default.isCollectionKey(key)) { return OnyxCache_1.default.get(key); } const allCacheKeys = OnyxCache_1.default.getAllKeys(); // It is possible we haven't loaded all keys yet so we do not know if the // collection actually exists. if (allCacheKeys.size === 0) { return; } const values = {}; allCacheKeys.forEach((cacheKey) => { if (!cacheKey.startsWith(key)) { return; } values[cacheKey] = OnyxCache_1.default.get(cacheKey); }); return values; } function useOnyx(key, options, dependencies = []) { const connectionRef = (0, react_1.useRef)(null); const previousKey = (0, usePrevious_1.default)(key); // Used to stabilize the selector reference and avoid unnecessary calls to `getSnapshot()`. const selectorRef = (0, useLiveRef_1.default)(options === null || options === void 0 ? void 0 : options.selector); // Stores the previous cached value as it's necessary to compare with the new value in `getSnapshot()`. // We initialize it to `null` to simulate that we don't have any value from cache yet. const previousValueRef = (0, react_1.useRef)(null); // Stores the newest cached value in order to compare with the previous one and optimize `getSnapshot()` execution. const newValueRef = (0, react_1.useRef)(null); // Stores the previously result returned by the hook, containing the data from cache and the fetch status. // We initialize it to `undefined` and `loading` fetch status to simulate the initial result when the hook is loading from the cache. // However, if `initWithStoredValues` is `false` we set the fetch status to `loaded` since we want to signal that data is ready. const resultRef = (0, react_1.useRef)([ undefined, { status: (options === null || options === void 0 ? void 0 : options.initWithStoredValues) === false ? 'loaded' : 'loading', }, ]); // Indicates if it's the first Onyx connection of this hook or not, as we don't want certain use cases // in `getSnapshot()` to be satisfied several times. const isFirstConnectionRef = (0, react_1.useRef)(true); // Indicates if the hook is connecting to an Onyx key. const isConnectingRef = (0, react_1.useRef)(false); // Stores the `onStoreChange()` function, which can be used to trigger a `getSnapshot()` update when desired. const onStoreChangeFnRef = (0, react_1.useRef)(null); // Indicates if we should get the newest cached value from Onyx during `getSnapshot()` execution. const shouldGetCachedValueRef = (0, react_1.useRef)(true); // Inside useOnyx.ts, we need to track the sourceValue separately const sourceValueRef = (0, react_1.useRef)(undefined); (0, react_1.useEffect)(() => { // These conditions will ensure we can only handle dynamic collection member keys from the same collection. if ((options === null || options === void 0 ? void 0 : options.allowDynamicKey) || previousKey === key) { return; } try { const previousCollectionKey = OnyxUtils_1.default.splitCollectionMemberKey(previousKey)[0]; const collectionKey = OnyxUtils_1.default.splitCollectionMemberKey(key)[0]; if (OnyxUtils_1.default.isCollectionMemberKey(previousCollectionKey, previousKey, previousCollectionKey.length) && OnyxUtils_1.default.isCollectionMemberKey(collectionKey, key, collectionKey.length) && previousCollectionKey === collectionKey) { return; } } catch (e) { throw new Error(`'${previousKey}' key can't be changed to '${key}'. useOnyx() only supports dynamic keys if they are both collection member keys from the same collection e.g. from 'collection_id1' to 'collection_id2'.`); } throw new Error(`'${previousKey}' key can't be changed to '${key}'. useOnyx() only supports dynamic keys if they are both collection member keys from the same collection e.g. from 'collection_id1' to 'collection_id2'.`); }, [previousKey, key, options === null || options === void 0 ? void 0 : options.allowDynamicKey]); (0, react_1.useEffect)(() => { // This effect will only run if the `dependencies` array changes. If it changes it will force the hook // to trigger a `getSnapshot()` update by calling the stored `onStoreChange()` function reference, thus // re-running the hook and returning the latest value to the consumer. if (connectionRef.current === null || isConnectingRef.current || !onStoreChangeFnRef.current) { return; } shouldGetCachedValueRef.current = true; onStoreChangeFnRef.current(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [...dependencies]); // Mimics withOnyx's checkEvictableKeys() behavior. const checkEvictableKey = (0, react_1.useCallback)(() => { if ((options === null || options === void 0 ? void 0 : options.canEvict) === undefined || !connectionRef.current) { return; } if (!OnyxCache_1.default.isEvictableKey(key)) { throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({evictableKeys: []}).`); } if (options.canEvict) { OnyxConnectionManager_1.default.removeFromEvictionBlockList(connectionRef.current); } else { OnyxConnectionManager_1.default.addToEvictionBlockList(connectionRef.current); } }, [key, options === null || options === void 0 ? void 0 : options.canEvict]); const getSnapshot = (0, react_1.useCallback)(() => { var _a, _b, _c; let isOnyxValueDefined = true; // We return the initial result right away during the first connection if `initWithStoredValues` is set to `false`. if (isFirstConnectionRef.current && (options === null || options === void 0 ? void 0 : options.initWithStoredValues) === false) { return resultRef.current; } // We get the value from cache while the first connection to Onyx is being made, // so we can return any cached value right away. After the connection is made, we only // update `newValueRef` when `Onyx.connect()` callback is fired. if (isFirstConnectionRef.current || shouldGetCachedValueRef.current) { // Gets the value from cache and maps it with selector. It changes `null` to `undefined` for `useOnyx` compatibility. const value = tryGetCachedValue(key); const selectedValue = selectorRef.current ? selectorRef.current(value) : value; newValueRef.current = (selectedValue !== null && selectedValue !== void 0 ? selectedValue : undefined); // This flag is `false` when the original Onyx value (without selector) is not defined yet. // It will be used later to check if we need to log an alert that the value is missing. isOnyxValueDefined = value !== null && value !== undefined; // We set this flag to `false` again since we don't want to get the newest cached value every time `getSnapshot()` is executed, // and only when `Onyx.connect()` callback is fired. shouldGetCachedValueRef.current = false; } const hasCacheForKey = OnyxCache_1.default.hasCacheForKey(key); // Since the fetch status can be different given the use cases below, we define the variable right away. let newFetchStatus; // If we have pending merge operations for the key during the first connection, we set the new value to `undefined` // and fetch status to `loading` to simulate that it is still being loaded until we have the most updated data. // If `allowStaleData` is `true` this logic will be ignored and cached value will be used, even if it's stale data. if (isFirstConnectionRef.current && OnyxUtils_1.default.hasPendingMergeForKey(key) && !(options === null || options === void 0 ? void 0 : options.allowStaleData)) { newValueRef.current = undefined; newFetchStatus = 'loading'; } // If data is not present in cache and `initialValue` is set during the first connection, // we set the new value to `initialValue` and fetch status to `loaded` since we already have some data to return to the consumer. if (isFirstConnectionRef.current && !hasCacheForKey && (options === null || options === void 0 ? void 0 : options.initialValue) !== undefined) { newValueRef.current = options.initialValue; newFetchStatus = 'loaded'; } // We do a deep equality check if `selector` is defined, since each `tryGetCachedValue()` call will // generate a plain new primitive/object/array that was created using the `selector` function. // For the other cases we will only deal with object reference checks, so just a shallow equality check is enough. let areValuesEqual; if (selectorRef.current) { areValuesEqual = (0, fast_equals_1.deepEqual)((_a = previousValueRef.current) !== null && _a !== void 0 ? _a : undefined, newValueRef.current); } else { areValuesEqual = (0, fast_equals_1.shallowEqual)((_b = previousValueRef.current) !== null && _b !== void 0 ? _b : undefined, newValueRef.current); } // We update the cached value and the result in the following conditions: // We will update the cached value and the result in any of the following situations: // - The previously cached value is different from the new value. // - The previously cached value is `null` (not set from cache yet) and we have cache for this key // OR we have a pending `Onyx.clear()` task (if `Onyx.clear()` is running cache might not be available anymore // so we update the cached value/result right away in order to prevent infinite loading state issues). const shouldUpdateResult = !areValuesEqual || (previousValueRef.current === null && (hasCacheForKey || OnyxCache_1.default.hasPendingTask(OnyxCache_1.TASK.CLEAR))); if (shouldUpdateResult) { previousValueRef.current = newValueRef.current; // If the new value is `null` we default it to `undefined` to ensure the consumer gets a consistent result from the hook. const newStatus = newFetchStatus !== null && newFetchStatus !== void 0 ? newFetchStatus : 'loaded'; resultRef.current = [ (_c = previousValueRef.current) !== null && _c !== void 0 ? _c : undefined, { status: newStatus, sourceValue: sourceValueRef.current, }, ]; // If `canBeMissing` is set to `false` and the Onyx value of that key is not defined, // we log an alert so it can be acknowledged by the consumer. Additionally, we won't log alerts // if there's a `Onyx.clear()` task in progress. if ((options === null || options === void 0 ? void 0 : options.canBeMissing) === false && newStatus === 'loaded' && !isOnyxValueDefined && !OnyxCache_1.default.hasPendingTask(OnyxCache_1.TASK.CLEAR)) { Logger.logAlert(`useOnyx returned no data for key with canBeMissing set to false.`, { key, showAlert: true }); } } return resultRef.current; }, [options === null || options === void 0 ? void 0 : options.initWithStoredValues, options === null || options === void 0 ? void 0 : options.allowStaleData, options === null || options === void 0 ? void 0 : options.initialValue, options === null || options === void 0 ? void 0 : options.canBeMissing, key, selectorRef]); const subscribe = (0, react_1.useCallback)((onStoreChange) => { isConnectingRef.current = true; onStoreChangeFnRef.current = onStoreChange; connectionRef.current = OnyxConnectionManager_1.default.connect({ key, callback: (value, callbackKey, sourceValue) => { isConnectingRef.current = false; onStoreChangeFnRef.current = onStoreChange; // Signals that the first connection was made, so some logics in `getSnapshot()` // won't be executed anymore. isFirstConnectionRef.current = false; // Signals that we want to get the newest cached value again in `getSnapshot()`. shouldGetCachedValueRef.current = true; // sourceValue is unknown type, so we need to cast it to the correct type. sourceValueRef.current = sourceValue; // Finally, we signal that the store changed, making `getSnapshot()` be called again. onStoreChange(); }, initWithStoredValues: options === null || options === void 0 ? void 0 : options.initWithStoredValues, waitForCollectionCallback: OnyxUtils_1.default.isCollectionKey(key), reuseConnection: options === null || options === void 0 ? void 0 : options.reuseConnection, }); checkEvictableKey(); return () => { if (!connectionRef.current) { return; } OnyxConnectionManager_1.default.disconnect(connectionRef.current); isFirstConnectionRef.current = false; isConnectingRef.current = false; onStoreChangeFnRef.current = null; }; }, [key, options === null || options === void 0 ? void 0 : options.initWithStoredValues, options === null || options === void 0 ? void 0 : options.reuseConnection, checkEvictableKey]); const getSnapshotDecorated = (0, react_1.useMemo)(() => { if (!GlobalSettings.isPerformanceMetricsEnabled()) { return getSnapshot; } return (0, metrics_1.default)(getSnapshot, 'useOnyx.getSnapshot'); }, [getSnapshot]); (0, react_1.useEffect)(() => { checkEvictableKey(); }, [checkEvictableKey]); const result = (0, react_1.useSyncExternalStore)(subscribe, getSnapshotDecorated); return result; } exports.default = useOnyx;