react-native-onyx
Version:
State management for React Native
298 lines (297 loc) • 18.6 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 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 selector = options === null || options === void 0 ? void 0 : options.selector;
// Create memoized version of selector for performance
const memoizedSelector = (0, react_1.useMemo)(() => {
if (!selector) {
return null;
}
let lastInput;
let lastOutput;
let lastDependencies = [];
let hasComputed = false;
return (input) => {
const currentDependencies = currentDependenciesRef.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 (selector) {
const newOutput = selector(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, 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,
}), [options === null || options === void 0 ? void 0 : options.selector, options === null || options === void 0 ? void 0 : options.initWithStoredValues]);
(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]);
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]);
// Tracks the last memoizedSelector reference that getSnapshot() has computed with.
// When the selector changes, this mismatch forces getSnapshot() to re-evaluate
// even if all other conditions (isFirstConnection, shouldGetCachedValue, key) are false.
const lastComputedSelectorRef = (0, react_1.useRef)(memoizedSelector);
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;
}
}
// 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 or if the key has changed,
// so we can return any cached value right away. For the case where the key has changed, If we don't return the cached value right away, then the UI will show the incorrect (previous) value for a brief period which looks like a UI glitch to the user. After the connection is made, we only
// update `newValueRef` when `Onyx.connect()` callback is fired.
const hasSelectorChanged = lastComputedSelectorRef.current !== memoizedSelector;
if (isFirstConnectionRef.current || shouldGetCachedValueRef.current || key !== previousKey || hasSelectorChanged) {
// 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;
lastComputedSelectorRef.current = memoizedSelector;
newValueRef.current = (selectedValue !== null && selectedValue !== void 0 ? selectedValue : 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 (isFirstConnectionRef.current && OnyxUtils_1.default.hasPendingMergeForKey(key)) {
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
// OR the subscriber is triggered (the value is gotten from the storage)
// 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) || !isFirstConnectionRef.current));
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.
newFetchStatus = newFetchStatus !== null && newFetchStatus !== void 0 ? newFetchStatus : 'loaded';
resultRef.current = [
(_d = previousValueRef.current) !== null && _d !== void 0 ? _d : undefined,
{
status: newFetchStatus,
sourceValue: sourceValueRef.current,
},
];
}
if (newFetchStatus !== 'loading') {
OnyxSnapshotCache_1.default.setCachedResult(key, cacheKey, resultRef.current);
}
return resultRef.current;
}, [options === null || options === void 0 ? void 0 : options.initWithStoredValues, key, memoizedSelector, cacheKey, previousKey]);
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;