Skip to content

Commit

Permalink
Merge branch 'main' into feat/useonyx-metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
adhorodyski authored Dec 9, 2024
2 parents 2b658e8 + c25f9a0 commit 47c97b0
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 5 deletions.
36 changes: 35 additions & 1 deletion lib/useOnyx.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {deepEqual, shallowEqual} from 'fast-equals';
import {useCallback, useEffect, useMemo, useRef, useSyncExternalStore} from 'react';
import type {DependencyList} from 'react';
import OnyxCache, {TASK} from './OnyxCache';
import type {Connection} from './OnyxConnectionManager';
import connectionManager from './OnyxConnectionManager';
Expand Down Expand Up @@ -106,12 +107,18 @@ function getCachedValue<TKey extends OnyxKey, TValue>(key: TKey, selector?: UseO
function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
key: TKey,
options?: BaseUseOnyxOptions & UseOnyxInitialValueOption<TReturnValue> & Required<UseOnyxSelectorOption<TKey, TReturnValue>>,
dependencies?: DependencyList,
): UseOnyxResult<TReturnValue>;
function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
key: TKey,
options?: BaseUseOnyxOptions & UseOnyxInitialValueOption<NoInfer<TReturnValue>>,
dependencies?: DependencyList,
): UseOnyxResult<TReturnValue>;
function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(key: TKey, options?: UseOnyxOptions<TKey, TReturnValue>): UseOnyxResult<TReturnValue> {
function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
key: TKey,
options?: UseOnyxOptions<TKey, TReturnValue>,
dependencies: DependencyList = [],
): UseOnyxResult<TReturnValue> {
const connectionRef = useRef<Connection | null>(null);
const previousKey = usePrevious(key);

Expand Down Expand Up @@ -139,6 +146,12 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(key: TKey
// in `getSnapshot()` to be satisfied several times.
const isFirstConnectionRef = useRef(true);

// Indicates if the hook is connecting to an Onyx key.
const isConnectingRef = useRef(false);

// Stores the `onStoreChange()` function, which can be used to trigger a `getSnapshot()` update when desired.
const onStoreChangeFnRef = useRef<(() => void) | null>(null);

// Indicates if we should get the newest cached value from Onyx during `getSnapshot()` execution.
const shouldGetCachedValueRef = useRef(true);

Expand Down Expand Up @@ -170,6 +183,19 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(key: TKey
);
}, [previousKey, key]);

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 = useCallback(() => {
if (options?.canEvict === undefined || !connectionRef.current) {
Expand Down Expand Up @@ -257,9 +283,15 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(key: TKey

const subscribe = useCallback(
(onStoreChange: () => void) => {
isConnectingRef.current = true;
onStoreChangeFnRef.current = onStoreChange;

connectionRef.current = connectionManager.connect<CollectionKeyBase>({
key,
callback: () => {
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;
Expand All @@ -284,6 +316,8 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(key: TKey

connectionManager.disconnect(connectionRef.current);
isFirstConnectionRef.current = false;
isConnectingRef.current = false;
onStoreChangeFnRef.current = null;
};
},
[key, options?.initWithStoredValues, options?.reuseConnection, checkEvictableKey],
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-onyx",
"version": "2.0.83",
"version": "2.0.85",
"author": "Expensify, Inc.",
"homepage": "https://expensify.com",
"description": "State management for React Native",
Expand All @@ -25,7 +25,8 @@
"README.md",
"LICENSE.md"
],
"main": "lib/index.ts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"lint": "eslint .",
"typecheck": "tsc --noEmit",
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/useOnyxTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,55 @@ describe('useOnyx', () => {
});
});

describe('dependencies', () => {
it('should return the updated selected value when a external value passed to the dependencies list changes', async () => {
Onyx.mergeCollection(ONYXKEYS.COLLECTION.TEST_KEY, {
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'},
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'},
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry3`]: {id: 'entry3_id', name: 'entry3_name'},
} as GenericCollection);

let externalValue = 'ex1';

const {result, rerender} = renderHook(() =>
useOnyx(
ONYXKEYS.COLLECTION.TEST_KEY,
{
// @ts-expect-error bypass
selector: (entries: OnyxCollection<{id: string; name: string}>) =>
Object.entries(entries ?? {}).reduce<NonNullable<OnyxCollection<string>>>((acc, [key, value]) => {
acc[key] = `${value?.id}_${externalValue}`;
return acc;
}, {}),
},
[externalValue],
),
);

await act(async () => waitForPromisesToResolve());

expect(result.current[0]).toEqual({
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: 'entry1_id_ex1',
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: 'entry2_id_ex1',
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry3`]: 'entry3_id_ex1',
});
expect(result.current[1].status).toEqual('loaded');

externalValue = 'ex2';

await act(async () => {
rerender(undefined);
});

expect(result.current[0]).toEqual({
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: 'entry1_id_ex2',
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: 'entry2_id_ex2',
[`${ONYXKEYS.COLLECTION.TEST_KEY}entry3`]: 'entry3_id_ex2',
});
expect(result.current[1].status).toEqual('loaded');
});
});

// This test suite must be the last one to avoid problems when running the other tests here.
describe('canEvict', () => {
const error = (key: string) => `canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`;
Expand Down

0 comments on commit 47c97b0

Please sign in to comment.