react-native-onyx
Version:
State management for React Native
304 lines (303 loc) • 19.7 kB
JavaScript
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;
;