Skip to content

Commit e55975c

Browse files
authored
Merge pull request #735 from callstack-internal/fix/selector-on-dependency-list
Fix/Ensure the new selector execution
2 parents 50086f7 + 7b81568 commit e55975c

2 files changed

Lines changed: 32 additions & 9 deletions

File tree

lib/useOnyx.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
7373
const selector = options?.selector;
7474

7575
// Create memoized version of selector for performance
76-
const memoizedSelector = useMemo(() => {
76+
const memoizedSelector = useMemo((): UseOnyxSelector<TKey, TReturnValue> | null => {
7777
if (!selector) {
7878
return null;
7979
}
@@ -220,6 +220,11 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
220220
}
221221
}, [key, options?.canEvict]);
222222

223+
// Tracks the last memoizedSelector reference that getSnapshot() has computed with.
224+
// When the selector changes, this mismatch forces getSnapshot() to re-evaluate
225+
// even if all other conditions (isFirstConnection, shouldGetCachedValue, key) are false.
226+
const lastComputedSelectorRef = useRef(memoizedSelector);
227+
223228
const getSnapshot = useCallback(() => {
224229
// Check if we have any cache for this Onyx key
225230
// Don't use cache for first connection with initWithStoredValues: false
@@ -244,10 +249,12 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
244249
// We get the value from cache while the first connection to Onyx is being made or if the key has changed,
245250
// 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
246251
// update `newValueRef` when `Onyx.connect()` callback is fired.
247-
if (isFirstConnectionRef.current || shouldGetCachedValueRef.current || key !== previousKey) {
252+
const hasSelectorChanged = lastComputedSelectorRef.current !== memoizedSelector;
253+
if (isFirstConnectionRef.current || shouldGetCachedValueRef.current || key !== previousKey || hasSelectorChanged) {
248254
// Gets the value from cache and maps it with selector. It changes `null` to `undefined` for `useOnyx` compatibility.
249255
const value = OnyxUtils.tryGetCachedValue(key) as OnyxValue<TKey>;
250256
const selectedValue = memoizedSelector ? memoizedSelector(value) : value;
257+
lastComputedSelectorRef.current = memoizedSelector;
251258
newValueRef.current = (selectedValue ?? undefined) as TReturnValue | undefined;
252259

253260
// We set this flag to `false` again since we don't want to get the newest cached value every time `getSnapshot()` is executed,

tests/unit/useOnyxTest.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,10 @@ describe('useOnyx', () => {
429429
it('should always use the current selector reference to return new data', async () => {
430430
Onyx.set(ONYXKEYS.TEST_KEY, {id: 'test_id', name: 'test_name'});
431431

432-
let selector = ((entry: OnyxEntry<{id: string; name: string}>) => `id - ${entry?.id}, name - ${entry?.name}`) as UseOnyxSelector<OnyxKey, string>;
432+
let selector: UseOnyxSelector<OnyxKey, string> = ((entry: OnyxEntry<{id: string; name: string}>) => `id - ${entry?.id}, name - ${entry?.name}`) as UseOnyxSelector<
433+
OnyxKey,
434+
string
435+
>;
433436

434437
const {result, rerender} = renderHook(() =>
435438
useOnyx(ONYXKEYS.TEST_KEY, {
@@ -440,10 +443,22 @@ describe('useOnyx', () => {
440443
expect(result.current[0]).toEqual('id - test_id, name - test_name');
441444
expect(result.current[1].status).toEqual('loaded');
442445

443-
selector = ((entry: OnyxEntry<{id: string; name: string}>) => `id - ${entry?.id}, name - ${entry?.name} - selector changed`) as UseOnyxSelector<OnyxKey, string>;
446+
selector = ((entry: OnyxEntry<{id: string; name: string}>) => `id - ${entry?.id}, name - ${entry?.name} - selector changed synchronously`) as UseOnyxSelector<OnyxKey, string>;
447+
444448
rerender(undefined);
445449

446-
expect(result.current[0]).toEqual('id - test_id, name - test_name - selector changed');
450+
expect(result.current[0]).toEqual('id - test_id, name - test_name - selector changed synchronously');
451+
expect(result.current[1].status).toEqual('loaded');
452+
453+
selector = ((entry: OnyxEntry<{id: string; name: string}>) => `id - ${entry?.id}, name - ${entry?.name} - selector changed after macrotask`) as UseOnyxSelector<OnyxKey, string>;
454+
455+
await act(async () => {
456+
// This is necessary to avoid regressions of the selector interleaving bug, see https://github.com/Expensify/App/issues/79449
457+
await waitForPromisesToResolve();
458+
rerender(undefined);
459+
});
460+
461+
expect(result.current[0]).toEqual('id - test_id, name - test_name - selector changed after macrotask');
447462
expect(result.current[1].status).toEqual('loaded');
448463
});
449464

@@ -684,15 +699,16 @@ describe('useOnyx', () => {
684699

685700
const dependencies = ['constant'];
686701
let selectorCallCount = 0;
702+
const selector = ((data) => {
703+
selectorCallCount++;
704+
return `${dependencies.join(',')}:${(data as {value?: string})?.value}`;
705+
}) as UseOnyxSelector<OnyxKey, string>;
687706

688707
const {result, rerender} = renderHook(() =>
689708
useOnyx(
690709
ONYXKEYS.TEST_KEY,
691710
{
692-
selector: (data) => {
693-
selectorCallCount++;
694-
return `${dependencies.join(',')}:${(data as {value?: string})?.value}`;
695-
},
711+
selector,
696712
},
697713
dependencies,
698714
),

0 commit comments

Comments
 (0)