Skip to content

Commit 1276506

Browse files
runway-github[bot]gambinishabretonc7snickewansmithdylanbutler1
authored
chore(runway): cherry-pick fix: PerpsMarketList navigation, and performance optimizations in TabList cp-7.59.0 (#22458)
- 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] > Fixes navigation to the correct market tab (including `stocks_and_commodities`) and refactors `PerpsMarketListView` for smoother swipe/tab sync with memoized row items. > > - **Navigation/Filtering**: > - `PerpsHomeView`: pass `marketType="stocks_and_commodities"` for Stocks & Commodities section. > - `PerpsMarketTypeSection`: add `stocks_and_commodities` type; "See All" navigates with `defaultMarketTypeFilter` matching section type. > - **Market List View (performance/UX)**: > - Replace per-tab filtering/components with unified `displayMarkets` (applies sub-filter only on `stocks_and_commodities`). > - Implement programmatic scroll guard with `isScrollingProgrammatically` to prevent tab/scroll feedback loops and sync active tab on swipe/press. > - Simplify tabs rendering: render list per tab using `displayMarkets`; remove `tabsToRender` and related logic. > - Update empty/error/loading and analytics conditions to use `displayMarkets`; fade-in tied to `displayMarkets.length`. > - **Components**: > - `PerpsMarketRowItem`: export `React.memo(...)` for row memoization; adjust tests to rerender with spread props. > - **Tests**: > - Update expectations for "See All" navigation to pass the specific `defaultMarketTypeFilter` and accommodate memoized row rerenders. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2fc991c. 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> [9e5d761](9e5d761) Co-authored-by: Nick Gambino <35090461+gambinish@users.noreply.github.com> 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 9aec0aa commit 1276506

6 files changed

Lines changed: 75 additions & 120 deletions

File tree

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)}
@@ -540,7 +475,7 @@ const PerpsMarketListView = ({
540475
/>
541476
)}
542477

543-
{/* Tab Content - Scrollable */}
478+
{/* Tab Content - Swipeable */}
544479
<ScrollView
545480
ref={tabScrollViewRef}
546481
horizontal
@@ -553,26 +488,40 @@ const PerpsMarketListView = ({
553488
}}
554489
style={styles.tabScrollView}
555490
>
556-
{tabsToRender.map((tabContent, index) => (
491+
{tabsData.map((tab) => (
557492
<View
558-
key={tabsData[index]?.key || index}
493+
key={tab.key}
559494
style={[
560495
styles.tabContentContainer,
561496
{ width: containerWidth },
562497
]}
563498
>
564-
{tabContent}
499+
<Animated.View
500+
style={[
501+
styles.animatedListContainer,
502+
{ opacity: fadeAnimation },
503+
]}
504+
>
505+
<PerpsMarketList
506+
markets={displayMarkets}
507+
onMarketPress={handleMarketPress}
508+
sortBy={sortBy}
509+
showBadge={false}
510+
contentContainerStyle={styles.tabContentContainer}
511+
testID={`${PerpsMarketListViewSelectorsIDs.MARKET_LIST}-${tab.filter}`}
512+
/>
513+
</Animated.View>
565514
</View>
566515
))}
567516
</ScrollView>
568517
</View>
569518
)}
570519

571-
{/* Market list hidden when tabs are shown (tabs contain the list) */}
520+
{/* Market list when no tabs shown (rare case) */}
572521
{!isSearchVisible &&
573522
!isLoadingMarkets &&
574523
!error &&
575-
tabsToRender.length === 0 && (
524+
tabsData.length === 0 && (
576525
<View style={styles.listContainerWithTabBar}>
577526
{renderMarketList()}
578527
</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
@@ -206,4 +206,4 @@ const PerpsMarketRowItem = ({
206206
);
207207
};
208208

209-
export default PerpsMarketRowItem;
209+
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)