react-native-onyx
Version:
State management for React Native
258 lines (257 loc) • 16.5 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 (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;
;