react-native-onyx 2.0.135 → 2.0.136

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,6 +42,7 @@ const OnyxUtils_1 = __importDefault(require("./OnyxUtils"));
42
42
  const Str = __importStar(require("./Str"));
43
43
  const utils_1 = __importDefault(require("./utils"));
44
44
  const OnyxCache_1 = __importDefault(require("./OnyxCache"));
45
+ const OnyxSnapshotCache_1 = __importDefault(require("./OnyxSnapshotCache"));
45
46
  /**
46
47
  * Manages Onyx connections of `Onyx.connect()`, `useOnyx()` and `withOnyx()` subscribers.
47
48
  */
@@ -182,12 +183,16 @@ class OnyxConnectionManager {
182
183
  });
183
184
  });
184
185
  this.connectionsMap.clear();
186
+ // Clear snapshot cache when all connections are disconnected
187
+ OnyxSnapshotCache_1.default.clear();
185
188
  }
186
189
  /**
187
190
  * Refreshes the connection manager's session ID.
188
191
  */
189
192
  refreshSessionID() {
190
193
  this.sessionID = Str.guid();
194
+ // Clear snapshot cache when session refreshes to avoid stale cache issues
195
+ OnyxSnapshotCache_1.default.clear();
191
196
  }
192
197
  /**
193
198
  * Adds the connection to the eviction block list. Connections added to this list can never be evicted.
@@ -0,0 +1,78 @@
1
+ import type { OnyxKey, OnyxValue } from './types';
2
+ import type { UseOnyxOptions, UseOnyxResult, UseOnyxSelector } from './useOnyx';
3
+ /**
4
+ * Manages snapshot caching for useOnyx hook performance optimization.
5
+ * Handles selector function tracking and memoized getSnapshot results.
6
+ */
7
+ declare class OnyxSnapshotCache {
8
+ /**
9
+ * Snapshot cache is a two-level map. The top-level keys are Onyx keys. The top-level values maps.
10
+ * The second-level keys are a custom composite string defined by this.registerConsumer. These represent a unique useOnyx config, which is not fully represented by the Onyx key alone.
11
+ * The reason we have two levels is for performance: not to make cache access faster, but to make cache invalidation faster.
12
+ * We can invalidate the snapshot cache for a given Onyx key with one map.delete operation on the top-level map, rather than having to loop through a large single-level map and delete any matching keys.
13
+ */
14
+ private snapshotCache;
15
+ /**
16
+ * Maps selector functions to unique IDs for cache key generation
17
+ */
18
+ private selectorIDMap;
19
+ /**
20
+ * Counter for generating incremental selector IDs
21
+ */
22
+ private selectorIDCounter;
23
+ /**
24
+ * Reference counting for cache keys to enable automatic cleanup.
25
+ * Maps cache key (string) to number of consumers using it.
26
+ */
27
+ private cacheKeyRefCounts;
28
+ constructor();
29
+ /**
30
+ * Generate unique ID for selector functions using incrementing numbers
31
+ */
32
+ getSelectorID<TKey extends OnyxKey, TReturnValue>(selector: UseOnyxSelector<TKey, TReturnValue>): number;
33
+ /**
34
+ * Register a consumer for a cache key and return the cache key.
35
+ * Generates cache key and increments reference counter.
36
+ *
37
+ * The properties used to generate the cache key are handpicked for performance reasons and
38
+ * according to their purpose and effect they produce in the useOnyx hook behavior:
39
+ *
40
+ * - `selector`: Different selectors produce different results, so each selector needs its own cache entry
41
+ * - `initWithStoredValues`: This flag changes the initial loading behavior and affects the returned fetch status
42
+ * - `allowStaleData`: Controls whether stale data can be returned during pending merges, affecting result timing
43
+ * - `canBeMissing`: Determines logging behavior for missing data, but doesn't affect the actual data returned
44
+ *
45
+ * Other options like `canEvict`, `reuseConnection`, and `allowDynamicKey` don't affect the data transformation
46
+ * or timing behavior of getSnapshot, so they're excluded from the cache key for better cache hit rates.
47
+ */
48
+ registerConsumer<TKey extends OnyxKey, TReturnValue>(options: Pick<UseOnyxOptions<TKey, TReturnValue>, 'selector' | 'initWithStoredValues' | 'allowStaleData' | 'canBeMissing'>): string;
49
+ /**
50
+ * Deregister a consumer for a cache key.
51
+ * Decrements reference counter and removes cache entry if no consumers remain.
52
+ */
53
+ deregisterConsumer(key: OnyxKey, cacheKey: string): void;
54
+ /**
55
+ * Get cached snapshot result for a key and cache key combination
56
+ */
57
+ getCachedResult<TResult extends UseOnyxResult<OnyxValue<OnyxKey>>>(key: OnyxKey, cacheKey: string): TResult | undefined;
58
+ /**
59
+ * Set cached snapshot result for a key and cache key combination
60
+ */
61
+ setCachedResult<TResult extends UseOnyxResult<OnyxValue<OnyxKey>>>(key: OnyxKey, cacheKey: string, result: TResult): void;
62
+ /**
63
+ * Selective cache invalidation to prevent data unavailability
64
+ * Collection members invalidate upward, collections don't cascade downward
65
+ */
66
+ invalidateForKey(keyToInvalidate: OnyxKey): void;
67
+ /**
68
+ * Clear all snapshot cache
69
+ */
70
+ clear(): void;
71
+ /**
72
+ * Clear selector ID mappings (useful for testing)
73
+ */
74
+ clearSelectorIds(): void;
75
+ }
76
+ declare const onyxSnapshotCache: OnyxSnapshotCache;
77
+ export default onyxSnapshotCache;
78
+ export { OnyxSnapshotCache };
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.OnyxSnapshotCache = void 0;
7
+ const OnyxUtils_1 = __importDefault(require("./OnyxUtils"));
8
+ /**
9
+ * Manages snapshot caching for useOnyx hook performance optimization.
10
+ * Handles selector function tracking and memoized getSnapshot results.
11
+ */
12
+ class OnyxSnapshotCache {
13
+ constructor() {
14
+ this.snapshotCache = new Map();
15
+ this.selectorIDMap = new Map();
16
+ this.selectorIDCounter = 0;
17
+ this.cacheKeyRefCounts = new Map();
18
+ }
19
+ /**
20
+ * Generate unique ID for selector functions using incrementing numbers
21
+ */
22
+ getSelectorID(selector) {
23
+ const typedSelector = selector;
24
+ if (!this.selectorIDMap.has(typedSelector)) {
25
+ const id = this.selectorIDCounter++;
26
+ this.selectorIDMap.set(typedSelector, id);
27
+ }
28
+ return this.selectorIDMap.get(typedSelector);
29
+ }
30
+ /**
31
+ * Register a consumer for a cache key and return the cache key.
32
+ * Generates cache key and increments reference counter.
33
+ *
34
+ * The properties used to generate the cache key are handpicked for performance reasons and
35
+ * according to their purpose and effect they produce in the useOnyx hook behavior:
36
+ *
37
+ * - `selector`: Different selectors produce different results, so each selector needs its own cache entry
38
+ * - `initWithStoredValues`: This flag changes the initial loading behavior and affects the returned fetch status
39
+ * - `allowStaleData`: Controls whether stale data can be returned during pending merges, affecting result timing
40
+ * - `canBeMissing`: Determines logging behavior for missing data, but doesn't affect the actual data returned
41
+ *
42
+ * Other options like `canEvict`, `reuseConnection`, and `allowDynamicKey` don't affect the data transformation
43
+ * or timing behavior of getSnapshot, so they're excluded from the cache key for better cache hit rates.
44
+ */
45
+ registerConsumer(options) {
46
+ var _a, _b, _c;
47
+ const selectorID = (options === null || options === void 0 ? void 0 : options.selector) ? this.getSelectorID(options.selector) : 'no_selector';
48
+ // Create options hash without expensive JSON.stringify
49
+ const initWithStoredValues = (_a = options === null || options === void 0 ? void 0 : options.initWithStoredValues) !== null && _a !== void 0 ? _a : true;
50
+ const allowStaleData = (_b = options === null || options === void 0 ? void 0 : options.allowStaleData) !== null && _b !== void 0 ? _b : false;
51
+ const canBeMissing = (_c = options === null || options === void 0 ? void 0 : options.canBeMissing) !== null && _c !== void 0 ? _c : true;
52
+ const cacheKey = `${selectorID}_${initWithStoredValues}_${allowStaleData}_${canBeMissing}`;
53
+ // Increment reference count for this cache key
54
+ const currentCount = this.cacheKeyRefCounts.get(cacheKey) || 0;
55
+ this.cacheKeyRefCounts.set(cacheKey, currentCount + 1);
56
+ return cacheKey;
57
+ }
58
+ /**
59
+ * Deregister a consumer for a cache key.
60
+ * Decrements reference counter and removes cache entry if no consumers remain.
61
+ */
62
+ deregisterConsumer(key, cacheKey) {
63
+ const currentCount = this.cacheKeyRefCounts.get(cacheKey) || 0;
64
+ if (currentCount <= 1) {
65
+ // Last consumer - remove from reference counter and cache
66
+ this.cacheKeyRefCounts.delete(cacheKey);
67
+ // Remove from snapshot cache
68
+ const keyCache = this.snapshotCache.get(key);
69
+ if (keyCache) {
70
+ keyCache.delete(cacheKey);
71
+ // If this was the last cache entry for this Onyx key, remove the key entirely
72
+ if (keyCache.size === 0) {
73
+ this.snapshotCache.delete(key);
74
+ }
75
+ }
76
+ }
77
+ else {
78
+ // Still has other consumers - just decrement count
79
+ this.cacheKeyRefCounts.set(cacheKey, currentCount - 1);
80
+ }
81
+ }
82
+ /**
83
+ * Get cached snapshot result for a key and cache key combination
84
+ */
85
+ getCachedResult(key, cacheKey) {
86
+ const keyCache = this.snapshotCache.get(key);
87
+ return keyCache === null || keyCache === void 0 ? void 0 : keyCache.get(cacheKey);
88
+ }
89
+ /**
90
+ * Set cached snapshot result for a key and cache key combination
91
+ */
92
+ setCachedResult(key, cacheKey, result) {
93
+ if (!this.snapshotCache.has(key)) {
94
+ this.snapshotCache.set(key, new Map());
95
+ }
96
+ this.snapshotCache.get(key).set(cacheKey, result);
97
+ }
98
+ /**
99
+ * Selective cache invalidation to prevent data unavailability
100
+ * Collection members invalidate upward, collections don't cascade downward
101
+ */
102
+ invalidateForKey(keyToInvalidate) {
103
+ // Always invalidate the exact key
104
+ this.snapshotCache.delete(keyToInvalidate);
105
+ try {
106
+ // Check if the key is a collection member and invalidate the collection base key
107
+ const collectionBaseKey = OnyxUtils_1.default.getCollectionKey(keyToInvalidate);
108
+ this.snapshotCache.delete(collectionBaseKey);
109
+ }
110
+ catch (e) {
111
+ // do nothing - this just means the key is not a collection member
112
+ }
113
+ }
114
+ /**
115
+ * Clear all snapshot cache
116
+ */
117
+ clear() {
118
+ this.snapshotCache.clear();
119
+ }
120
+ /**
121
+ * Clear selector ID mappings (useful for testing)
122
+ */
123
+ clearSelectorIds() {
124
+ this.selectorIDCounter = 0;
125
+ }
126
+ }
127
+ exports.OnyxSnapshotCache = OnyxSnapshotCache;
128
+ // Create and export a singleton instance
129
+ const onyxSnapshotCache = new OnyxSnapshotCache();
130
+ exports.default = onyxSnapshotCache;
package/dist/useOnyx.d.ts CHANGED
@@ -47,4 +47,4 @@ type ResultMetadata<TValue> = {
47
47
  type UseOnyxResult<TValue> = [NonNullable<TValue> | undefined, ResultMetadata<TValue>];
48
48
  declare function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(key: TKey, options?: UseOnyxOptions<TKey, TReturnValue>, dependencies?: DependencyList): UseOnyxResult<TReturnValue>;
49
49
  export default useOnyx;
50
- export type { FetchStatus, ResultMetadata, UseOnyxResult, UseOnyxOptions };
50
+ export type { FetchStatus, ResultMetadata, UseOnyxResult, UseOnyxOptions, UseOnyxSelector };
package/dist/useOnyx.js CHANGED
@@ -45,6 +45,7 @@ const GlobalSettings = __importStar(require("./GlobalSettings"));
45
45
  const usePrevious_1 = __importDefault(require("./usePrevious"));
46
46
  const metrics_1 = __importDefault(require("./metrics"));
47
47
  const Logger = __importStar(require("./Logger"));
48
+ const OnyxSnapshotCache_1 = __importDefault(require("./OnyxSnapshotCache"));
48
49
  const useLiveRef_1 = __importDefault(require("./useLiveRef"));
49
50
  function useOnyx(key, options, dependencies = []) {
50
51
  const connectionRef = (0, react_1.useRef)(null);
@@ -105,6 +106,14 @@ function useOnyx(key, options, dependencies = []) {
105
106
  const shouldGetCachedValueRef = (0, react_1.useRef)(true);
106
107
  // Inside useOnyx.ts, we need to track the sourceValue separately
107
108
  const sourceValueRef = (0, react_1.useRef)(undefined);
109
+ // Cache the options key to avoid regenerating it every getSnapshot call
110
+ const cacheKey = (0, react_1.useMemo)(() => OnyxSnapshotCache_1.default.registerConsumer({
111
+ selector: options === null || options === void 0 ? void 0 : options.selector,
112
+ initWithStoredValues: options === null || options === void 0 ? void 0 : options.initWithStoredValues,
113
+ allowStaleData: options === null || options === void 0 ? void 0 : options.allowStaleData,
114
+ canBeMissing: options === null || options === void 0 ? void 0 : options.canBeMissing,
115
+ }), [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]);
116
+ (0, react_1.useEffect)(() => () => OnyxSnapshotCache_1.default.deregisterConsumer(key, cacheKey), [key, cacheKey]);
108
117
  (0, react_1.useEffect)(() => {
109
118
  // These conditions will ensure we can only handle dynamic collection member keys from the same collection.
110
119
  if ((options === null || options === void 0 ? void 0 : options.allowDynamicKey) || previousKey === key) {
@@ -137,6 +146,8 @@ function useOnyx(key, options, dependencies = []) {
137
146
  if (connectionRef.current === null || isConnectingRef.current || !onStoreChangeFnRef.current) {
138
147
  return;
139
148
  }
149
+ // Invalidate cache when dependencies change so selector runs with new closure values
150
+ OnyxSnapshotCache_1.default.invalidateForKey(key);
140
151
  shouldGetCachedValueRef.current = true;
141
152
  onStoreChangeFnRef.current();
142
153
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -158,10 +169,23 @@ function useOnyx(key, options, dependencies = []) {
158
169
  }, [key, options === null || options === void 0 ? void 0 : options.canEvict]);
159
170
  const getSnapshot = (0, react_1.useCallback)(() => {
160
171
  var _a, _b, _c, _d;
172
+ // Check if we have any cache for this Onyx key
173
+ // Don't use cache for first connection with initWithStoredValues: false
174
+ // Also don't use cache during active data updates (when shouldGetCachedValueRef is true)
175
+ if (!(isFirstConnectionRef.current && (options === null || options === void 0 ? void 0 : options.initWithStoredValues) === false) && !shouldGetCachedValueRef.current) {
176
+ const cachedResult = OnyxSnapshotCache_1.default.getCachedResult(key, cacheKey);
177
+ if (cachedResult !== undefined) {
178
+ resultRef.current = cachedResult;
179
+ return cachedResult;
180
+ }
181
+ }
161
182
  let isOnyxValueDefined = true;
162
183
  // We return the initial result right away during the first connection if `initWithStoredValues` is set to `false`.
163
184
  if (isFirstConnectionRef.current && (options === null || options === void 0 ? void 0 : options.initWithStoredValues) === false) {
164
- return resultRef.current;
185
+ const result = resultRef.current;
186
+ // Store result in snapshot cache
187
+ OnyxSnapshotCache_1.default.setCachedResult(key, cacheKey, result);
188
+ return result;
165
189
  }
166
190
  // We get the value from cache while the first connection to Onyx is being made,
167
191
  // so we can return any cached value right away. After the connection is made, we only
@@ -188,7 +212,7 @@ function useOnyx(key, options, dependencies = []) {
188
212
  newValueRef.current = undefined;
189
213
  newFetchStatus = 'loading';
190
214
  }
191
- // Optimized equality checking - eliminated redundant deep equality:
215
+ // Optimized equality checking:
192
216
  // - Memoized selectors already handle deep equality internally, so we can use fast reference equality
193
217
  // - Non-selector cases use shallow equality for object reference checks
194
218
  // - Normalize null to undefined to ensure consistent comparison (both represent "no value")
@@ -226,8 +250,9 @@ function useOnyx(key, options, dependencies = []) {
226
250
  Logger.logAlert(`useOnyx returned no data for key with canBeMissing set to false for key ${key}`, { showAlert: true });
227
251
  }
228
252
  }
253
+ OnyxSnapshotCache_1.default.setCachedResult(key, cacheKey, resultRef.current);
229
254
  return resultRef.current;
230
- }, [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]);
255
+ }, [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]);
231
256
  const subscribe = (0, react_1.useCallback)((onStoreChange) => {
232
257
  isConnectingRef.current = true;
233
258
  onStoreChangeFnRef.current = onStoreChange;
@@ -243,6 +268,8 @@ function useOnyx(key, options, dependencies = []) {
243
268
  shouldGetCachedValueRef.current = true;
244
269
  // sourceValue is unknown type, so we need to cast it to the correct type.
245
270
  sourceValueRef.current = sourceValue;
271
+ // Invalidate snapshot cache for this key when data changes
272
+ OnyxSnapshotCache_1.default.invalidateForKey(key);
246
273
  // Finally, we signal that the store changed, making `getSnapshot()` be called again.
247
274
  onStoreChange();
248
275
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "2.0.135",
3
+ "version": "2.0.136",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",