Skip to content

Commit 651201c

Browse files
authored
Merge pull request Expensify#83432 from Expensify/vit-removeDeepCompareRef-transactionList
Remove useDeepCompareRef from MoneyRequestReportTransactionList
2 parents 3880251 + 668f59c commit 651201c

3 files changed

Lines changed: 33 additions & 18 deletions

File tree

src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import Text from '@components/Text';
1414
import {useWideRHPActions} from '@components/WideRHPContextProvider';
1515
import useCopySelectionHelper from '@hooks/useCopySelectionHelper';
1616
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
17-
import useDeepCompareRef from '@hooks/useDeepCompareRef';
1817
import useHandleSelectionMode from '@hooks/useHandleSelectionMode';
1918
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
2019
import useLocalize from '@hooks/useLocalize';
@@ -284,17 +283,19 @@ function MoneyRequestReportTransactionList({
284283
return groupedTransactions.flatMap((group) => group.transactions.filter((transaction) => !isTransactionPendingDelete(transaction)).map((transaction) => transaction.transactionID));
285284
}, [groupedTransactions, sortedTransactions, shouldShowGroupedTransactions]);
286285

287-
const visualOrderTransactionIDsDeepCompare = useDeepCompareRef(visualOrderTransactionIDs);
288286
useEffect(() => {
289287
const focusedRoute = findFocusedRoute(navigationRef.getRootState());
290288
if (focusedRoute?.name !== SCREENS.RIGHT_MODAL.SEARCH_REPORT) {
291289
return;
292290
}
293-
setActiveTransactionIDs(visualOrderTransactionIDsDeepCompare ?? []);
291+
setActiveTransactionIDs(visualOrderTransactionIDs);
292+
}, [visualOrderTransactionIDs]);
293+
294+
useEffect(() => {
294295
return () => {
295296
clearActiveTransactionIDs();
296297
};
297-
}, [visualOrderTransactionIDsDeepCompare]);
298+
}, []);
298299

299300
const groupSelectionState = useMemo(() => {
300301
const state = new Map<string, {isSelected: boolean; isIndeterminate: boolean; isDisabled: boolean; pendingAction?: PendingAction}>();

src/libs/actions/TransactionThreadNavigation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,23 @@ import ONYXKEYS from '@src/ONYXKEYS';
99
* We save this value in onyx, so that we can correctly display navigation UI in transaction thread RHP.
1010
*/
1111

12+
let lastSetIDs: string[] | null = null;
13+
14+
/**
15+
* Idempotent: skips the Onyx write when the IDs haven't changed.
16+
* This lets callers (e.g. useEffect in MoneyRequestReportTransactionList) fire
17+
* freely without worrying about referential equality of the input array.
18+
*/
1219
function setActiveTransactionIDs(ids: string[]) {
20+
if (lastSetIDs?.length === ids.length && lastSetIDs.every((id, i) => id === ids.at(i))) {
21+
return Promise.resolve();
22+
}
23+
lastSetIDs = ids;
1324
return Onyx.set(ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS, ids);
1425
}
1526

1627
function clearActiveTransactionIDs() {
28+
lastSetIDs = null;
1729
return Onyx.set(ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS, null);
1830
}
1931

tests/unit/MoneyRequestReportTransactionListActiveTransactionIDsTest.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {findFocusedRoute} from '@react-navigation/native';
22
import {renderHook} from '@testing-library/react-native';
33
import {useEffect} from 'react';
4-
import useDeepCompareRef from '@hooks/useDeepCompareRef';
54
import {clearActiveTransactionIDs, setActiveTransactionIDs} from '@libs/actions/TransactionThreadNavigation';
65
import {navigationRef} from '@libs/Navigation/Navigation';
76
import SCREENS from '@src/SCREENS';
@@ -30,20 +29,19 @@ jest.mock('@react-navigation/native', () => ({
3029
* to allow isolated testing of the useEffect behavior.
3130
*/
3231
function useActiveTransactionIDsEffect(visualOrderTransactionIDs: string[]) {
33-
const visualOrderTransactionIDsDeepCompare = useDeepCompareRef(visualOrderTransactionIDs);
34-
3532
useEffect(() => {
3633
const focusedRoute = findFocusedRoute(navigationRef.getRootState());
3734
if (focusedRoute?.name !== SCREENS.RIGHT_MODAL.SEARCH_REPORT) {
3835
return;
3936
}
40-
setActiveTransactionIDs(visualOrderTransactionIDsDeepCompare ?? []);
37+
setActiveTransactionIDs(visualOrderTransactionIDs);
38+
}, [visualOrderTransactionIDs]);
39+
40+
useEffect(() => {
4141
return () => {
4242
clearActiveTransactionIDs();
4343
};
44-
}, [visualOrderTransactionIDsDeepCompare]);
45-
46-
return {visualOrderTransactionIDsDeepCompare};
44+
}, []);
4745
}
4846

4947
describe('MoneyRequestReportTransactionList - Active Transaction IDs Effect', () => {
@@ -112,7 +110,7 @@ describe('MoneyRequestReportTransactionList - Active Transaction IDs Effect', ()
112110
expect(mockClearActiveTransactionIDs).toHaveBeenCalledTimes(1);
113111
});
114112

115-
it('should NOT call clearActiveTransactionIDs on unmount when route was NOT SEARCH_REPORT', () => {
113+
it('should call clearActiveTransactionIDs on unmount even when route was NOT SEARCH_REPORT', () => {
116114
// Given the focused route is NOT SEARCH_REPORT
117115
mockFindFocusedRoute.mockReturnValue({name: 'SomeOtherRoute', key: 'test-key'});
118116

@@ -123,11 +121,11 @@ describe('MoneyRequestReportTransactionList - Active Transaction IDs Effect', ()
123121

124122
unmount();
125123

126-
// Then clearActiveTransactionIDs should NOT be called (since the effect returned early)
127-
expect(mockClearActiveTransactionIDs).not.toHaveBeenCalled();
124+
// Then clearActiveTransactionIDs should still be called (cleanup runs on unmount regardless of route)
125+
expect(mockClearActiveTransactionIDs).toHaveBeenCalledTimes(1);
128126
});
129127

130-
it('should update active transaction IDs when the list changes (deep comparison)', () => {
128+
it('should update active transaction IDs when the list changes', () => {
131129
// Given the focused route is SEARCH_REPORT
132130
mockFindFocusedRoute.mockReturnValue({name: SCREENS.RIGHT_MODAL.SEARCH_REPORT, key: 'test-key'});
133131

@@ -150,7 +148,7 @@ describe('MoneyRequestReportTransactionList - Active Transaction IDs Effect', ()
150148
expect(mockSetActiveTransactionIDs).toHaveBeenLastCalledWith(newTransactionIDs);
151149
});
152150

153-
it('should NOT update when transaction IDs array has same content (deep comparison)', () => {
151+
it('should call setActiveTransactionIDs on reference change without clearing first (idempotent guard is in the action layer)', () => {
154152
// Given the focused route is SEARCH_REPORT
155153
mockFindFocusedRoute.mockReturnValue({name: SCREENS.RIGHT_MODAL.SEARCH_REPORT, key: 'test-key'});
156154

@@ -167,8 +165,12 @@ describe('MoneyRequestReportTransactionList - Active Transaction IDs Effect', ()
167165
const sameContentNewArray = ['trans1', 'trans2'];
168166
rerender({ids: sameContentNewArray});
169167

170-
// Then setActiveTransactionIDs should NOT be called again (deep comparison prevents it)
171-
expect(mockSetActiveTransactionIDs).toHaveBeenCalledTimes(1);
168+
// Then the effect fires (new reference), but setActiveTransactionIDs internally skips the Onyx write.
169+
// Crucially, clearActiveTransactionIDs is NOT called on re-render (only on unmount),
170+
// preventing the null→same-IDs flash.
171+
expect(mockSetActiveTransactionIDs).toHaveBeenCalledTimes(2);
172+
expect(mockSetActiveTransactionIDs).toHaveBeenLastCalledWith(sameContentNewArray);
173+
expect(mockClearActiveTransactionIDs).not.toHaveBeenCalled();
172174
});
173175

174176
it('should handle empty transaction IDs array', () => {

0 commit comments

Comments
 (0)