react-native-onyx
Version:
State management for React Native
244 lines (243 loc) • 14.9 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 OnyxSnapshotCache_1 = __importDefault(require("./OnyxSnapshotCache"));
const useLiveRef_1 = __importDefault(require("./useLiveRef"));
function useOnyx(key, options, dependencies = []) {
const connectionRef = (0, react_1.useRef)(null);
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) {
const newOutput = selector(input);
// Always track the current input to avoid re-running the selector
// when the same input is seen again (even if the output didn't change).
lastInput = input;
// Only update the output reference if it actually changed
if (!hasComputed || !(0, fast_equals_1.deepEqual)(lastOutput, newOutput) || dependenciesChanged) {
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.
const resultRef = (0, react_1.useRef)([
undefined,
{
status: 'loading',
},
]);
// Tracks which key has completed its first Onyx connection callback. When this doesn't match the
// current key, getSnapshot() treats the hook as being in its "first connection" state for that key.
// This is key-aware by design: when the key changes, connectedKeyRef still holds the old key (or null
// after cleanup), so the hook automatically enters first-connection mode for the new key without any
// explicit reset logic — eliminating the race condition where cleanup could clobber a boolean flag.
const connectedKeyRef = (0, react_1.useRef)(null);
// Tracks whether the hook has completed its initial mount subscription.
// Unlike connectedKeyRef (which gets nulled by cleanup), this persists across re-subscriptions.
const hasMountedRef = (0, react_1.useRef)(false);
// 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);
// Cache the options key to avoid regenerating it every getSnapshot call
const cacheKey = (0, react_1.useMemo)(() => OnyxSnapshotCache_1.default.registerConsumer(key, {
selector: options === null || options === void 0 ? void 0 : options.selector,
}), [key, options === null || options === void 0 ? void 0 : options.selector]);
(0, react_1.useEffect)(() => () => OnyxSnapshotCache_1.default.deregisterConsumer(key, cacheKey), [key, cacheKey]);
// 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 || connectedKeyRef.current !== key || !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]);
// 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;
// Check if we have any cache for this Onyx key
// Don't use cache during active data updates (when shouldGetCachedValueRef is true)
const isFirstConnection = connectedKeyRef.current !== key;
if (!shouldGetCachedValueRef.current) {
const cachedResult = OnyxSnapshotCache_1.default.getCachedResult(key, cacheKey);
if (cachedResult !== undefined) {
resultRef.current = cachedResult;
return cachedResult;
}
}
// 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 (isFirstConnection || shouldGetCachedValueRef.current || 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 (isFirstConnection && OnyxUtils_1.default.hasPendingMergeForKey(key)) {
newValueRef.current = undefined;
newFetchStatus = 'loading';
}
// shallowEqual checks === first (O(1) for frozen snapshots and stable selector references),
// then falls back to comparing top-level properties for individual keys that may have
// new references with equivalent content.
// Normalize null to undefined to ensure consistent comparison (both represent "no value").
const areValuesEqual = (0, fast_equals_1.shallowEqual)((_a = previousValueRef.current) !== null && _a !== void 0 ? _a : undefined, (_b = newValueRef.current) !== null && _b !== void 0 ? _b : undefined);
// 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) || !isFirstConnection));
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 = [
(_c = previousValueRef.current) !== null && _c !== void 0 ? _c : undefined,
{
status: newFetchStatus,
},
];
}
if (newFetchStatus !== 'loading') {
OnyxSnapshotCache_1.default.setCachedResult(key, cacheKey, resultRef.current);
}
return resultRef.current;
}, [key, memoizedSelector, cacheKey]);
const subscribe = (0, react_1.useCallback)((onStoreChange) => {
// Reset internal state so the hook properly transitions through loading
// for the new key instead of preserving stale state from the previous one.
// Only reset when the key has actually changed (not on initial mount).
if (hasMountedRef.current) {
previousValueRef.current = null;
newValueRef.current = null;
resultRef.current = [undefined, { status: 'loading' }];
shouldGetCachedValueRef.current = true;
}
hasMountedRef.current = true;
isConnectingRef.current = true;
onStoreChangeFnRef.current = onStoreChange;
connectionRef.current = OnyxConnectionManager_1.default.connect({
key,
callback: () => {
isConnectingRef.current = false;
onStoreChangeFnRef.current = onStoreChange;
// Signals that the first connection was made for this key, so some logics
// in `getSnapshot()` won't be executed anymore.
connectedKeyRef.current = key;
// Signals that we want to get the newest cached value again in `getSnapshot()`.
shouldGetCachedValueRef.current = true;
// 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();
},
reuseConnection: options === null || options === void 0 ? void 0 : options.reuseConnection,
});
return () => {
if (!connectionRef.current) {
return;
}
OnyxConnectionManager_1.default.disconnect(connectionRef.current);
connectedKeyRef.current = null;
isConnectingRef.current = false;
onStoreChangeFnRef.current = null;
};
}, [key, options === null || options === void 0 ? void 0 : options.reuseConnection]);
const result = (0, react_1.useSyncExternalStore)(subscribe, getSnapshot);
return result;
}
exports.default = useOnyx;