UNPKG

react-native-onyx

Version:

State management for React Native

304 lines (303 loc) 19.7 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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __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 usePrevious_1 = __importDefault(require("./usePrevious")); const metrics_1 = __importDefault(require("./metrics")); const Logger = __importStar(require("./Logger")); const OnyxSnapshotCache_1 = __importDefault(require("./OnyxSnapshotCache")); const useLiveRef_1 = __importDefault(require("./useLiveRef")); function useOnyx(key, options, dependencies = []) { const connectionRef = (0, react_1.useRef)(null); const previousKey = (0, usePrevious_1.default)(key); const currentDependenciesRef = (0, useLiveRef_1.default)(dependencies); const currentSelectorRef = (0, useLiveRef_1.default)(options === null || options === void 0 ? void 0 : options.selector); // Create memoized version of selector for performance const memoizedSelector = (0, react_1.useMemo)(() => { if (!(options === null || options === void 0 ? void 0 : options.selector)) return null; let lastInput; let lastOutput; let lastDependencies = []; let hasComputed = false; return (input) => { const currentDependencies = currentDependenciesRef.current; const currentSelector = currentSelectorRef.current; // Recompute if input changed, dependencies changed, or first time const dependenciesChanged = !(0, fast_equals_1.shallowEqual)(lastDependencies, currentDependencies); if (!hasComputed || lastInput !== input || dependenciesChanged) { // Only proceed if we have a valid selector if (currentSelector) { const newOutput = currentSelector(input); // Deep equality mode: only update if output actually changed if (!hasComputed || !(0, fast_equals_1.deepEqual)(lastOutput, newOutput) || dependenciesChanged) { lastInput = input; lastOutput = newOutput; lastDependencies = [...currentDependencies]; hasComputed = true; } } } return lastOutput; }; }, [currentDependenciesRef, currentSelectorRef, 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); // Cache the options key to avoid regenerating it every getSnapshot call const cacheKey = (0, react_1.useMemo)(() => OnyxSnapshotCache_1.default.registerConsumer({ selector: options === null || options === void 0 ? void 0 : options.selector, initWithStoredValues: options === null || options === void 0 ? void 0 : options.initWithStoredValues, allowStaleData: options === null || options === void 0 ? void 0 : options.allowStaleData, canBeMissing: options === null || options === void 0 ? void 0 : options.canBeMissing, }), [options === null || options === void 0 ? void 0 : options.selector, 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.canBeMissing]); (0, react_1.useEffect)(() => () => OnyxSnapshotCache_1.default.deregisterConsumer(key, cacheKey), [key, cacheKey]); (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) && OnyxUtils_1.default.isCollectionMemberKey(collectionKey, key) && 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]); // Track previous dependencies to prevent infinite loops const previousDependenciesRef = (0, react_1.useRef)([]); (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. // Deep equality check to prevent infinite loops when dependencies array reference changes // but content remains the same if ((0, fast_equals_1.shallowEqual)(previousDependenciesRef.current, dependencies)) { return; } previousDependenciesRef.current = dependencies; if (connectionRef.current === null || isConnectingRef.current || !onStoreChangeFnRef.current) { return; } // Invalidate cache when dependencies change so selector runs with new closure values OnyxSnapshotCache_1.default.invalidateForKey(key); 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, _d; // Check if we have any cache for this Onyx key // Don't use cache for first connection with initWithStoredValues: false // Also don't use cache during active data updates (when shouldGetCachedValueRef is true) if (!(isFirstConnectionRef.current && (options === null || options === void 0 ? void 0 : options.initWithStoredValues) === false) && !shouldGetCachedValueRef.current) { const cachedResult = OnyxSnapshotCache_1.default.getCachedResult(key, cacheKey); if (cachedResult !== undefined) { resultRef.current = cachedResult; return cachedResult; } } 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) { const result = resultRef.current; // Store result in snapshot cache OnyxSnapshotCache_1.default.setCachedResult(key, cacheKey, result); return result; } // 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 = OnyxUtils_1.default.tryGetCachedValue(key); const selectedValue = memoizedSelector ? memoizedSelector(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'; } // Optimized equality checking: // - Memoized selectors already handle deep equality internally, so we can use fast reference equality // - Non-selector cases use shallow equality for object reference checks // - Normalize null to undefined to ensure consistent comparison (both represent "no value") let areValuesEqual; if (memoizedSelector) { const normalizedPrevious = (_a = previousValueRef.current) !== null && _a !== void 0 ? _a : undefined; const normalizedNew = (_b = newValueRef.current) !== null && _b !== void 0 ? _b : undefined; areValuesEqual = normalizedPrevious === normalizedNew; } else { areValuesEqual = (0, fast_equals_1.shallowEqual)((_c = previousValueRef.current) !== null && _c !== void 0 ? _c : 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 = [ (_d = previousValueRef.current) !== null && _d !== void 0 ? _d : 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 for key ${key}`, { showAlert: true }); } } OnyxSnapshotCache_1.default.setCachedResult(key, cacheKey, resultRef.current); 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.canBeMissing, key, memoizedSelector, cacheKey]); 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; // Invalidate snapshot cache for this key when data changes OnyxSnapshotCache_1.default.invalidateForKey(key); // 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;