Skip to content

Commit 9e5d761

Browse files
gambinishabretonc7snickewansmithdylanbutler1
authored
fix: PerpsMarketList navigation, and performance optimizations in TabList cp-7.59.0 (#22341)
## **Description** Navigating from a subsection in the PerpHomeScreen should navigate to the proper tab in the MarketList. This PR also introduces some performance optimizations in the horizontal scroll view by memoizing list items to reduce the memory footprint, leading to a snappier behavior. If we want further optimizations, we can remove swipe navigation, in favor of just pressing the tabs and leaning into lazy loading more. ## **Changelog** CHANGELOG entry: Fix PerpsMarketList navigation and improve performance on swipeable list view ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TAT-2039 ## **Manual testing steps** ```gherkin Feature: Perps Market List Tab Navigation Scenario: user navigates to Stocks tab from home screen Explore stocks and commodities section Given user is on the Perps home screen And the "Explore stocks and commodities" section is visible When user taps "See all" in the "Explore stocks and commodities" section Then user is navigated to the Perps market list screen And the "Stocks" tab is selected And stocks and commodities markets are displayed Scenario: user navigates to Crypto tab from home screen Explore crypto section Given user is on the Perps home screen And the "Explore crypto" section is visible When user taps "See all" in the "Explore crypto" section Then user is navigated to the Perps market list screen And the "Crypto" tab is selected And crypto markets are displayed Scenario: user switches tabs by tapping tab bar Given user is on the Perps market list screen And the "All" tab is currently selected And the market list is scrolled to the middle When user taps the "Crypto" tab Then the "Crypto" tab becomes active And only crypto markets are displayed And the market list is scrolled to the top And no performance lag is observed Scenario: user switches tabs by swiping Given user is on the Perps market list screen And the "All" tab is currently selected When user swipes left on the market list Then the "Crypto" tab becomes active And the tab bar indicator animates to "Crypto" And only crypto markets are displayed And the swipe gesture is smooth without stuttering Scenario: user swipes between multiple tabs quickly Given user is on the Perps market list screen And the "All" tab is currently selected When user swipes left to the "Crypto" tab And user swipes left again to the "Stocks" tab Then the "Stocks" tab becomes active And the tab bar indicator animates smoothly to "Stocks" And stocks and commodities markets are displayed And tab switching is instant without noticeable delay Scenario: user returns to previously viewed tab Given user is on the Perps market list screen And the "Crypto" tab is currently selected And user has previously viewed the "All" tab When user taps the "All" tab Then the "All" tab becomes active And all markets (crypto, stocks, and commodities) are displayed And the market list is scrolled to the top And no re-rendering delay is observed Scenario: user applies sub-filter on Stocks tab Given user is on the Perps market list screen And the "Stocks" tab is currently selected And both stocks and commodities are displayed When user taps the stocks/commodities filter dropdown And user selects "Stocks only" Then only equity markets are displayed And commodity markets are hidden And the filter updates instantly Scenario: user switches away from Stocks tab with active sub-filter Given user is on the Perps market list screen And the "Stocks" tab is currently selected And the sub-filter is set to "Stocks only" When user taps the "Crypto" tab And user taps back to the "Stocks" tab Then the "Stocks" tab becomes active And the sub-filter is reset to "All" And both stocks and commodities are displayed ``` ## **Screenshots/Recordings** https://github.com/user-attachments/assets/9b8c5278-018a-4a42-89b6-ef0cbd3b647a ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Routes "See all" to the correct market tab and refactors MarketListView with swipe/tab sync fixes, simplified filtering, and memoized row items for better performance. > > - **Perps Home & Sections**: > - `PerpsHomeView`: pass `marketType="stocks_and_commodities"` to Stocks & Commodities section. > - `PerpsMarketTypeSection`: support `marketType="stocks_and_commodities"`; on "See All" navigate with `defaultMarketTypeFilter` set to provided `marketType`. > - **Market List View**: > - Simplify tab filtering: introduce `displayMarkets` applying sub-filter only on `stocks_and_commodities`; replace checks/usages from `filteredMarkets` → `displayMarkets`. > - Tabs rendering: inline tab content; remove `getFilteredMarketsForTab`/`MarketTypeTabContent` and `tabsToRender`. > - Swipe/tab sync: add `isScrollingProgrammatically` guard; sync `ScrollView` position on tab change; handle scroll to update `marketTypeFilter` without feedback loops. > - Active tab mapping: include `stocks_and_commodities` and legacy `equity`/`commodity`. > - Update measurement and event tracking conditions to use `displayMarkets.length`. > - **Performance**: > - `PerpsMarketRowItem`: export as `React.memo` to reduce re-renders. > - **Tests**: > - Update navigation assertions in `PerpsMarketTypeSection.test.tsx` to expect specific `defaultMarketTypeFilter`. > - Adjust `PerpsMarketRowItem.test.tsx` rerenders to pass new props copies. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5921889. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: abretonc7s <107169956+abretonc7s@users.noreply.github.com> Co-authored-by: Arthur Breton <arthur.breton@consensys.net> Co-authored-by: Nicholas Smith <nick.smith@consensys.net> Co-authored-by: dylanbutler1 <99672693+dylanbutler1@users.noreply.github.com>
1 parent f0e76d0 commit 9e5d761

File tree

6 files changed

+75
-120
lines changed

6 files changed

+75
-120
lines changed

app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ const PerpsHomeView = () => {
258258
<PerpsMarketTypeSection
259259
title={strings('perps.home.stocks_and_commodities')}
260260
markets={stocksAndCommoditiesMarkets}
261-
marketType="all"
261+
marketType="stocks_and_commodities"
262262
sortBy={sortBy}
263263
isLoading={isLoading.markets}
264264
/>

app/components/UI/Perps/Views/PerpsMarketListView/PerpsMarketListView.tsx

Lines changed: 56 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const PerpsMarketListView = ({
7474

7575
const fadeAnimation = useRef(new Animated.Value(0)).current;
7676
const tabScrollViewRef = useRef<ScrollView>(null);
77+
const isScrollingProgrammatically = useRef(false);
7778
const [isSortFieldSheetVisible, setIsSortFieldSheetVisible] = useState(false);
7879
const [isStocksCommoditiesSheetVisible, setIsStocksCommoditiesSheetVisible] =
7980
useState(false);
@@ -132,83 +133,20 @@ const PerpsMarketListView = ({
132133
[onMarketSelect, perpsNavigation, route.params?.source],
133134
);
134135

135-
// Get filtered markets for specific tab (used within each tab)
136-
const getFilteredMarketsForTab = useCallback(
137-
(filter: 'all' | 'crypto' | 'stocks_and_commodities') => {
138-
if (searchQuery.trim()) {
139-
// When searching, show all search results (filtering handled by search)
140-
return filteredMarkets;
141-
}
142-
143-
// Filter by tab when not searching
144-
if (filter === 'all') {
145-
// All = Crypto + Stocks + Commodities (excluding forex)
146-
return filteredMarkets.filter(
147-
(m) =>
148-
!m.marketType ||
149-
m.marketType === 'equity' ||
150-
m.marketType === 'commodity',
151-
);
152-
}
153-
if (filter === 'crypto') {
154-
// Crypto markets have no marketType set
155-
return filteredMarkets.filter((m) => !m.marketType);
156-
}
157-
if (filter === 'stocks_and_commodities') {
158-
// Combined stocks and commodities filter - apply sub-filter
159-
let stocksCommoditiesMarkets = filteredMarkets.filter(
160-
(m) => m.marketType === 'equity' || m.marketType === 'commodity',
161-
);
162-
163-
// Apply stocks/commodities sub-filter if not 'all'
164-
if (stocksCommoditiesFilter !== 'all') {
165-
stocksCommoditiesMarkets = stocksCommoditiesMarkets.filter(
166-
(m) => m.marketType === stocksCommoditiesFilter,
167-
);
168-
}
169-
170-
return stocksCommoditiesMarkets;
171-
}
172-
return filteredMarkets;
173-
},
174-
[filteredMarkets, searchQuery, stocksCommoditiesFilter],
175-
);
176-
177-
// Market type tab content component (filters markets by tab type)
178-
// tabLabel is extracted by TabsList component for display, not used here
179-
const MarketTypeTabContent = useCallback(
180-
({
181-
tabFilter,
182-
tabLabel: _tabLabel,
183-
}: {
184-
tabFilter: 'all' | 'crypto' | 'stocks_and_commodities';
185-
tabLabel: string;
186-
}) => {
187-
const tabMarkets = getFilteredMarketsForTab(tabFilter);
188-
return (
189-
<Animated.View
190-
style={[styles.animatedListContainer, { opacity: fadeAnimation }]}
191-
>
192-
<PerpsMarketList
193-
markets={tabMarkets}
194-
onMarketPress={handleMarketPress}
195-
sortBy={sortBy}
196-
showBadge={false}
197-
contentContainerStyle={styles.tabContentContainer}
198-
testID={`${PerpsMarketListViewSelectorsIDs.MARKET_LIST}-${tabFilter}`}
199-
/>
200-
</Animated.View>
136+
// Apply stocks/commodities sub-filter when on Stocks tab
137+
const displayMarkets = useMemo(() => {
138+
// If on stocks_and_commodities tab and sub-filter is active, apply it
139+
if (
140+
marketTypeFilter === 'stocks_and_commodities' &&
141+
stocksCommoditiesFilter !== 'all'
142+
) {
143+
return filteredMarkets.filter(
144+
(m) => m.marketType === stocksCommoditiesFilter,
201145
);
202-
},
203-
[
204-
getFilteredMarketsForTab,
205-
handleMarketPress,
206-
sortBy,
207-
fadeAnimation,
208-
styles.animatedListContainer,
209-
styles.tabContentContainer,
210-
],
211-
);
146+
}
147+
// Otherwise, use markets already filtered by the hook
148+
return filteredMarkets;
149+
}, [filteredMarkets, marketTypeFilter, stocksCommoditiesFilter]);
212150

213151
// Build tabs data for TabsBar
214152
const tabsData = useMemo(() => {
@@ -249,19 +187,6 @@ const PerpsMarketListView = ({
249187
return tabs;
250188
}, [marketCounts]);
251189

252-
// Build tab content components
253-
const tabsToRender = useMemo(
254-
() =>
255-
tabsData.map((tab) => (
256-
<MarketTypeTabContent
257-
key={tab.key}
258-
tabFilter={tab.filter}
259-
tabLabel={tab.label}
260-
/>
261-
)),
262-
[tabsData, MarketTypeTabContent],
263-
);
264-
265190
// Calculate active tab index from current marketTypeFilter
266191
const activeTabIndex = useMemo(() => {
267192
if (tabsData.length === 0) {
@@ -294,9 +219,14 @@ const PerpsMarketListView = ({
294219
[tabsData, setMarketTypeFilter],
295220
);
296221

297-
// Handle scroll to sync active tab
222+
// Handle scroll to sync active tab (for swipe gestures)
298223
const handleScroll = useCallback(
299224
(event: { nativeEvent: { contentOffset: { x: number } } }) => {
225+
// Ignore programmatic scrolls to prevent feedback loop with useEffect
226+
if (isScrollingProgrammatically.current) {
227+
return;
228+
}
229+
300230
const offsetX = event.nativeEvent.contentOffset.x;
301231
const index = Math.round(offsetX / containerWidth);
302232
if (index >= 0 && index < tabsData.length) {
@@ -309,29 +239,34 @@ const PerpsMarketListView = ({
309239
[containerWidth, tabsData, marketTypeFilter, setMarketTypeFilter],
310240
);
311241

312-
// Sync scroll position when active tab changes (e.g., from navigation param)
242+
// Sync scroll position when active tab changes (e.g., from tab bar press or navigation param)
313243
useEffect(() => {
314244
if (
315245
tabScrollViewRef.current &&
316246
activeTabIndex >= 0 &&
317247
tabsData.length > 0
318248
) {
249+
isScrollingProgrammatically.current = true;
319250
tabScrollViewRef.current.scrollTo({
320251
x: activeTabIndex * containerWidth,
321252
animated: true,
322253
});
254+
// Clear flag after animation completes (~300ms animation + 50ms buffer)
255+
setTimeout(() => {
256+
isScrollingProgrammatically.current = false;
257+
}, 350);
323258
}
324259
}, [activeTabIndex, containerWidth, tabsData.length]);
325260

326261
useEffect(() => {
327-
if (filteredMarkets.length > 0) {
262+
if (displayMarkets.length > 0) {
328263
Animated.timing(fadeAnimation, {
329264
toValue: 1,
330265
duration: 300,
331266
useNativeDriver: true,
332267
}).start();
333268
}
334-
}, [filteredMarkets.length, fadeAnimation]);
269+
}, [displayMarkets.length, fadeAnimation]);
335270

336271
// Reset stocks/commodities filter to 'all' when switching tabs
337272
// This ensures that when switching to the Stocks tab, it always shows both stocks and commodities
@@ -364,15 +299,15 @@ const PerpsMarketListView = ({
364299
// Performance tracking: Measure screen load time until market data is displayed
365300
usePerpsMeasurement({
366301
traceName: TraceName.PerpsMarketListView,
367-
conditions: [filteredMarkets.length > 0],
302+
conditions: [displayMarkets.length > 0],
368303
});
369304

370305
// Track markets screen viewed event
371306
const source =
372307
route.params?.source || PerpsEventValues.SOURCE.MAIN_ACTION_BUTTON;
373308
usePerpsEventTracking({
374309
eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED,
375-
conditions: [filteredMarkets.length > 0],
310+
conditions: [displayMarkets.length > 0],
376311
properties: {
377312
[PerpsEventProperties.SCREEN_TYPE]: PerpsEventValues.SCREEN_TYPE.MARKETS,
378313
[PerpsEventProperties.SOURCE]: source,
@@ -397,7 +332,7 @@ const PerpsMarketListView = ({
397332
}
398333

399334
// Error (Failed to load markets)
400-
if (error && filteredMarkets.length === 0) {
335+
if (error && displayMarkets.length === 0) {
401336
return (
402337
<View style={styles.errorContainer}>
403338
<Text
@@ -415,7 +350,7 @@ const PerpsMarketListView = ({
415350
}
416351

417352
// Empty favorites results - show when favorites filter is active but no favorites found
418-
if (showFavoritesOnly && filteredMarkets.length === 0) {
353+
if (showFavoritesOnly && displayMarkets.length === 0) {
419354
return (
420355
<View style={styles.emptyStateContainer}>
421356
<Icon
@@ -443,7 +378,7 @@ const PerpsMarketListView = ({
443378
}
444379

445380
// Empty search results - show when search is visible and no markets match
446-
if (isSearchVisible && filteredMarkets.length === 0) {
381+
if (isSearchVisible && displayMarkets.length === 0) {
447382
return (
448383
<View style={styles.emptyStateContainer}>
449384
<Icon
@@ -478,7 +413,7 @@ const PerpsMarketListView = ({
478413
style={[styles.animatedListContainer, { opacity: fadeAnimation }]}
479414
>
480415
<PerpsMarketList
481-
markets={filteredMarkets}
416+
markets={displayMarkets}
482417
onMarketPress={handleMarketPress}
483418
sortBy={sortBy}
484419
showBadge={false}
@@ -518,7 +453,7 @@ const PerpsMarketListView = ({
518453
tabs={tabsData.map((tab) => ({
519454
key: tab.key,
520455
label: tab.label,
521-
content: null, // Content is rendered separately in ScrollView
456+
content: null,
522457
isDisabled: false,
523458
}))}
524459
activeIndex={activeTabIndex}
@@ -527,7 +462,7 @@ const PerpsMarketListView = ({
527462
/>
528463

529464
{/* Filter Bar - Between tabs and content */}
530-
{(filteredMarkets.length > 0 || showFavoritesOnly) && (
465+
{(displayMarkets.length > 0 || showFavoritesOnly) && (
531466
<PerpsMarketFiltersBar
532467
selectedOptionId={selectedOptionId}
533468
onSortPress={() => setIsSortFieldSheetVisible(true)}
@@ -542,7 +477,7 @@ const PerpsMarketListView = ({
542477
/>
543478
)}
544479

545-
{/* Tab Content - Scrollable */}
480+
{/* Tab Content - Swipeable */}
546481
<ScrollView
547482
ref={tabScrollViewRef}
548483
horizontal
@@ -555,26 +490,40 @@ const PerpsMarketListView = ({
555490
}}
556491
style={styles.tabScrollView}
557492
>
558-
{tabsToRender.map((tabContent, index) => (
493+
{tabsData.map((tab) => (
559494
<View
560-
key={tabsData[index]?.key || index}
495+
key={tab.key}
561496
style={[
562497
styles.tabContentContainer,
563498
{ width: containerWidth },
564499
]}
565500
>
566-
{tabContent}
501+
<Animated.View
502+
style={[
503+
styles.animatedListContainer,
504+
{ opacity: fadeAnimation },
505+
]}
506+
>
507+
<PerpsMarketList
508+
markets={displayMarkets}
509+
onMarketPress={handleMarketPress}
510+
sortBy={sortBy}
511+
showBadge={false}
512+
contentContainerStyle={styles.tabContentContainer}
513+
testID={`${PerpsMarketListViewSelectorsIDs.MARKET_LIST}-${tab.filter}`}
514+
/>
515+
</Animated.View>
567516
</View>
568517
))}
569518
</ScrollView>
570519
</View>
571520
)}
572521

573-
{/* Market list hidden when tabs are shown (tabs contain the list) */}
522+
{/* Market list when no tabs shown (rare case) */}
574523
{!isSearchVisible &&
575524
!isLoadingMarkets &&
576525
!error &&
577-
tabsToRender.length === 0 && (
526+
tabsData.length === 0 && (
578527
<View style={styles.listContainerWithTabBar}>
579528
{renderMarketList()}
580529
</View>

app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,21 +390,21 @@ describe('PerpsMarketRowItem', () => {
390390
mockUsePerpsLivePrices.mockReturnValue({
391391
BTC: { price: '50000', volume24h: 750000000 },
392392
});
393-
rerender(<PerpsMarketRowItem market={mockMarketData} />);
393+
rerender(<PerpsMarketRowItem market={{ ...mockMarketData }} />);
394394
expect(screen.getByText('$750.00M')).toBeOnTheScreen(); // M shows 2 decimals
395395

396396
// Test thousands (0 decimals with formatVolume)
397397
mockUsePerpsLivePrices.mockReturnValue({
398398
BTC: { price: '50000', volume24h: 50000 },
399399
});
400-
rerender(<PerpsMarketRowItem market={mockMarketData} />);
400+
rerender(<PerpsMarketRowItem market={{ ...mockMarketData }} />);
401401
expect(screen.getByText('$50K')).toBeOnTheScreen(); // K shows no decimals
402402

403403
// Test small values (2 decimals with formatVolume)
404404
mockUsePerpsLivePrices.mockReturnValue({
405405
BTC: { price: '50000', volume24h: 123.45 },
406406
});
407-
rerender(<PerpsMarketRowItem market={mockMarketData} />);
407+
rerender(<PerpsMarketRowItem market={{ ...mockMarketData }} />);
408408
expect(screen.getByText('$123.45')).toBeOnTheScreen(); // Shows 2 decimals
409409
});
410410

app/components/UI/Perps/components/PerpsMarketRowItem/PerpsMarketRowItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,4 @@ const PerpsMarketRowItem = ({
211211
);
212212
};
213213

214-
export default PerpsMarketRowItem;
214+
export default React.memo(PerpsMarketRowItem);

app/components/UI/Perps/components/PerpsMarketTypeSection/PerpsMarketTypeSection.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ describe('PerpsMarketTypeSection', () => {
242242
expect(mockNavigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, {
243243
screen: Routes.PERPS.MARKET_LIST,
244244
params: {
245-
defaultMarketTypeFilter: 'all',
245+
defaultMarketTypeFilter: 'crypto',
246246
},
247247
});
248248
});
@@ -261,7 +261,7 @@ describe('PerpsMarketTypeSection', () => {
261261
expect(mockNavigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, {
262262
screen: Routes.PERPS.MARKET_LIST,
263263
params: {
264-
defaultMarketTypeFilter: 'all',
264+
defaultMarketTypeFilter: 'equity',
265265
},
266266
});
267267
});
@@ -280,7 +280,7 @@ describe('PerpsMarketTypeSection', () => {
280280
expect(mockNavigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, {
281281
screen: Routes.PERPS.MARKET_LIST,
282282
params: {
283-
defaultMarketTypeFilter: 'all',
283+
defaultMarketTypeFilter: 'commodity',
284284
},
285285
});
286286
});

0 commit comments

Comments
 (0)