From 5e6766bab7cb4b8dbf65a56432e10b5f44223d2b Mon Sep 17 00:00:00 2001
From: IF <139582705+infiniteflower@users.noreply.github.com>
Date: Wed, 15 Jul 2026 19:07:37 -0400
Subject: [PATCH 1/4] chore: lift selectDestToken and useDisplayCurrencyValue
out of each quote row
---
.../QuoteSelectorView/QuoteList.test.tsx | 32 ++++++++++++-
.../QuoteSelectorView/QuoteList.tsx | 47 +++++++++++++++++--
.../components/QuoteSelectorView/QuoteRow.tsx | 38 +++++++++++----
3 files changed, 103 insertions(+), 14 deletions(-)
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx
index bbc02441901..ca6cffdf816 100644
--- a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx
@@ -2,9 +2,11 @@ import React from 'react';
import { render } from '@testing-library/react-native';
import { QuoteList } from './QuoteList';
import { QuoteRowProps } from './QuoteRow';
+import { useSelector } from 'react-redux';
+import { getDisplayCurrencyValue } from '../../utils/exchange-rates';
jest.mock('./QuoteRow', () => ({
- QuoteRow: ({ provider, quoteRequestId }: QuoteRowProps) => {
+ QuoteRowView: ({ provider, quoteRequestId }: QuoteRowProps) => {
const { Text } = jest.requireActual('react-native');
return (
@@ -14,10 +16,25 @@ jest.mock('./QuoteRow', () => ({
},
}));
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest.fn(() => undefined),
+}));
+
+jest.mock('../../utils/exchange-rates', () => ({
+ getDisplayCurrencyValue: jest.fn(() => '$0.00'),
+}));
+
jest.mock('../../hooks/useShouldRenderGasSponsoredBanner', () => ({
useShouldRenderGasSponsoredBanner: jest.fn(),
}));
+const mockUseSelector = useSelector as jest.MockedFunction;
+const mockGetDisplayCurrencyValue =
+ getDisplayCurrencyValue as jest.MockedFunction<
+ typeof getDisplayCurrencyValue
+ >;
+
describe('QuoteList', () => {
const mockOnPress = jest.fn();
@@ -36,6 +53,19 @@ describe('QuoteList', () => {
});
describe('rendering', () => {
+ it('selects currency data once for multiple rows', () => {
+ const quotes = [
+ createMockQuote({ quoteRequestId: 'quote-1' }),
+ createMockQuote({ quoteRequestId: 'quote-2' }),
+ createMockQuote({ quoteRequestId: 'quote-3' }),
+ ];
+
+ render();
+
+ expect(mockUseSelector).toHaveBeenCalledTimes(6);
+ expect(mockGetDisplayCurrencyValue).toHaveBeenCalledTimes(3);
+ });
+
it('renders empty list when data is empty array', () => {
const { queryByTestId } = render();
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx
index 73c2ca6c91d..114cbf369a1 100644
--- a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx
@@ -1,9 +1,50 @@
import React from 'react';
-import { QuoteRow, QuoteRowProps } from './QuoteRow';
+import { useSelector } from 'react-redux';
+import { QuoteRowProps, QuoteRowView } from './QuoteRow';
+import { selectDestToken } from '../../../../../core/redux/slices/bridge';
+import { selectTokenMarketData } from '../../../../../selectors/tokenRatesController';
+import {
+ selectCurrencyRates,
+ selectCurrentCurrency,
+} from '../../../../../selectors/currencyRateController';
+import { selectNetworkConfigurations } from '../../../../../selectors/networkController';
+///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
+import { selectMultichainAssetsRates } from '../../../../../selectors/multichain';
+///: END:ONLY_INCLUDE_IF(keyring-snaps)
+import { getDisplayCurrencyValue } from '../../utils/exchange-rates';
interface Props {
data: QuoteRowProps[];
}
-export const QuoteList = ({ data }: Props) =>
- data.map((quote) => );
+export const QuoteList = ({ data }: Props) => {
+ const destToken = useSelector(selectDestToken);
+ const evmMultiChainMarketData = useSelector(selectTokenMarketData);
+ const evmMultiChainCurrencyRates = useSelector(selectCurrencyRates);
+ const networkConfigurationsByChainId = useSelector(
+ selectNetworkConfigurations,
+ );
+ const currentCurrency = useSelector(selectCurrentCurrency);
+
+ let nonEvmMultichainAssetRates = {};
+ ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
+ nonEvmMultichainAssetRates = useSelector(selectMultichainAssetsRates);
+ ///: END:ONLY_INCLUDE_IF(keyring-snaps)
+
+ return data.map((quote) => (
+
+ ));
+};
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx
index 759272b2c4a..5e9e8603de3 100644
--- a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx
@@ -38,30 +38,32 @@ export interface QuoteRowProps {
receiveAmount?: string;
}
-export const QuoteRow = ({
+export interface QuoteRowViewProps extends QuoteRowProps {
+ formattedReceiveAmountFiat: string;
+ destTokenSymbol?: string;
+}
+
+export const QuoteRowView = ({
selected,
provider,
isLowestCost,
formattedTotalCost,
+ formattedReceiveAmountFiat,
+ destTokenSymbol,
onPress,
quoteRequestId,
loading,
receiveAmount,
-}: QuoteRowProps) => {
+}: QuoteRowViewProps) => {
const theme = useTheme();
- const destToken = useSelector(selectDestToken);
- const formattedReceiveAmountFiat = useDisplayCurrencyValue(
- receiveAmount,
- destToken,
- );
const formattedReceiveAmount = useMemo(
() =>
- receiveAmount && receiveAmount !== '0' && destToken
+ receiveAmount && receiveAmount !== '0' && destTokenSymbol
? formatAmountWithLocaleSeparators(
limitToMaximumDecimalPlaces(parseFloat(receiveAmount)),
)
: receiveAmount,
- [receiveAmount, destToken],
+ [receiveAmount, destTokenSymbol],
);
return (
@@ -127,7 +129,7 @@ export const QuoteRow = ({
>
- ~ {formattedReceiveAmount} {destToken?.symbol}
+ ~ {formattedReceiveAmount} {destTokenSymbol}
@@ -143,3 +145,19 @@ export const QuoteRow = ({
);
};
+
+export const QuoteRow = (props: QuoteRowProps) => {
+ const destToken = useSelector(selectDestToken);
+ const formattedReceiveAmountFiat = useDisplayCurrencyValue(
+ props.receiveAmount,
+ destToken,
+ );
+
+ return (
+
+ );
+};
From 3cc7495e57e3d713fc96ac61f70114d9b1a7f0cd Mon Sep 17 00:00:00 2001
From: IF <139582705+infiniteflower@users.noreply.github.com>
Date: Wed, 15 Jul 2026 19:10:31 -0400
Subject: [PATCH 2/4] chore: memoize quote row view
---
.../QuoteSelectorView/QuoteRow.test.tsx | 8 +-
.../components/QuoteSelectorView/QuoteRow.tsx | 193 +++++++++---------
2 files changed, 106 insertions(+), 95 deletions(-)
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.test.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.test.tsx
index 2b958b99f57..c2ec4602b69 100644
--- a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.test.tsx
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.test.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
-import { QuoteRow, QuoteRowProps } from './QuoteRow';
+import { QuoteRow, QuoteRowProps, QuoteRowView } from './QuoteRow';
import { strings } from '../../../../../../locales/i18n';
import { BridgeToken } from '../../types';
import { CHAIN_IDS } from '@metamask/transaction-controller';
@@ -77,6 +77,12 @@ describe('QuoteRow', () => {
});
});
+ describe('memoization', () => {
+ it('memoizes QuoteRowView', () => {
+ expect(QuoteRowView).toHaveProperty('$$typeof', Symbol.for('react.memo'));
+ });
+ });
+
describe('rendering', () => {
it('renders provider name', () => {
// Act
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx
index 5e9e8603de3..da8c420ae0c 100644
--- a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx
@@ -43,108 +43,113 @@ export interface QuoteRowViewProps extends QuoteRowProps {
destTokenSymbol?: string;
}
-export const QuoteRowView = ({
- selected,
- provider,
- isLowestCost,
- formattedTotalCost,
- formattedReceiveAmountFiat,
- destTokenSymbol,
- onPress,
- quoteRequestId,
- loading,
- receiveAmount,
-}: QuoteRowViewProps) => {
- const theme = useTheme();
- const formattedReceiveAmount = useMemo(
- () =>
- receiveAmount && receiveAmount !== '0' && destTokenSymbol
- ? formatAmountWithLocaleSeparators(
- limitToMaximumDecimalPlaces(parseFloat(receiveAmount)),
- )
- : receiveAmount,
- [receiveAmount, destTokenSymbol],
- );
+export const QuoteRowView = React.memo(
+ ({
+ selected,
+ provider,
+ isLowestCost,
+ formattedTotalCost,
+ formattedReceiveAmountFiat,
+ destTokenSymbol,
+ onPress,
+ quoteRequestId,
+ loading,
+ receiveAmount,
+ }: QuoteRowViewProps) => {
+ const theme = useTheme();
+ const formattedReceiveAmount = useMemo(
+ () =>
+ receiveAmount && receiveAmount !== '0' && destTokenSymbol
+ ? formatAmountWithLocaleSeparators(
+ limitToMaximumDecimalPlaces(parseFloat(receiveAmount)),
+ )
+ : receiveAmount,
+ [receiveAmount, destTokenSymbol],
+ );
- return (
- onPress(quoteRequestId)}>
-
-
+ return (
+ onPress(quoteRequestId)}>
+
+
+
+
+
+
+ {provider.name}
+
+
+
+
+ {isLowestCost && !loading && (
+
+ {strings('bridge.lowest_cost')}
+
+ )}
+
+
+
+
+ {strings('bridge.total_cost')}: {formattedTotalCost}
+
+
+
-
-
- {provider.name}
-
-
+
+ ~ {formattedReceiveAmount} {destTokenSymbol}
+
+
+
+
+ ~ {formattedReceiveAmountFiat}
+
-
- {isLowestCost && !loading && (
-
- {strings('bridge.lowest_cost')}
-
- )}
-
-
-
- {strings('bridge.total_cost')}: {formattedTotalCost}
-
-
-
-
-
-
- ~ {formattedReceiveAmount} {destTokenSymbol}
-
-
-
-
- ~ {formattedReceiveAmountFiat}
-
-
-
-
- );
-};
+
+ );
+ },
+);
export const QuoteRow = (props: QuoteRowProps) => {
const destToken = useSelector(selectDestToken);
From fbcc70969da6fb33bc484951a415890eee881f72 Mon Sep 17 00:00:00 2001
From: IF <139582705+infiniteflower@users.noreply.github.com>
Date: Wed, 15 Jul 2026 19:20:55 -0400
Subject: [PATCH 3/4] refactor: update QuoteRow to use primitive providerName
prop
---
.../components/QuoteSelectorView/QuoteList.test.tsx | 6 +++---
.../Bridge/components/QuoteSelectorView/QuoteList.tsx | 3 ++-
.../Bridge/components/QuoteSelectorView/QuoteRow.tsx | 10 ++++++----
3 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx
index ca6cffdf816..dfc315aa86b 100644
--- a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx
@@ -1,16 +1,16 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { QuoteList } from './QuoteList';
-import { QuoteRowProps } from './QuoteRow';
+import { QuoteRowProps, QuoteRowViewProps } from './QuoteRow';
import { useSelector } from 'react-redux';
import { getDisplayCurrencyValue } from '../../utils/exchange-rates';
jest.mock('./QuoteRow', () => ({
- QuoteRowView: ({ provider, quoteRequestId }: QuoteRowProps) => {
+ QuoteRowView: ({ providerName, quoteRequestId }: QuoteRowViewProps) => {
const { Text } = jest.requireActual('react-native');
return (
- {provider.name} - {quoteRequestId}
+ {providerName} - {quoteRequestId}
);
},
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx
index 114cbf369a1..bc402842adb 100644
--- a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx
@@ -31,10 +31,11 @@ export const QuoteList = ({ data }: Props) => {
nonEvmMultichainAssetRates = useSelector(selectMultichainAssetsRates);
///: END:ONLY_INCLUDE_IF(keyring-snaps)
- return data.map((quote) => (
+ return data.map(({ provider, ...quote }) => (
{
+ providerName: string;
formattedReceiveAmountFiat: string;
destTokenSymbol?: string;
}
@@ -46,7 +47,7 @@ export interface QuoteRowViewProps extends QuoteRowProps {
export const QuoteRowView = React.memo(
({
selected,
- provider,
+ providerName,
isLowestCost,
formattedTotalCost,
formattedReceiveAmountFiat,
@@ -94,7 +95,7 @@ export const QuoteRowView = React.memo(
fontWeight={FontWeight.Medium}
variant={TextVariantDS.BodyMd}
>
- {provider.name}
+ {providerName}
@@ -151,7 +152,7 @@ export const QuoteRowView = React.memo(
},
);
-export const QuoteRow = (props: QuoteRowProps) => {
+export const QuoteRow = ({ provider, ...props }: QuoteRowProps) => {
const destToken = useSelector(selectDestToken);
const formattedReceiveAmountFiat = useDisplayCurrencyValue(
props.receiveAmount,
@@ -161,6 +162,7 @@ export const QuoteRow = (props: QuoteRowProps) => {
return (
From ad7bb4696fe6858cc6daf1050c32e4588e13fab1 Mon Sep 17 00:00:00 2001
From: IF <139582705+infiniteflower@users.noreply.github.com>
Date: Wed, 15 Jul 2026 19:46:36 -0400
Subject: [PATCH 4/4] chore: tests
---
.../QuoteSelectorView/QuoteList.test.tsx | 70 +++++++++++++++++--
1 file changed, 66 insertions(+), 4 deletions(-)
diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx
index dfc315aa86b..aed2f82a3a3 100644
--- a/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx
+++ b/app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.test.tsx
@@ -5,16 +5,33 @@ import { QuoteRowProps, QuoteRowViewProps } from './QuoteRow';
import { useSelector } from 'react-redux';
import { getDisplayCurrencyValue } from '../../utils/exchange-rates';
-jest.mock('./QuoteRow', () => ({
- QuoteRowView: ({ providerName, quoteRequestId }: QuoteRowViewProps) => {
+const mockQuoteRowRenderCounts = new Map();
+
+jest.mock('./QuoteRow', () => {
+ const ReactActual = jest.requireActual('react');
+
+ function MockQuoteRowView({
+ providerName,
+ quoteRequestId,
+ }: QuoteRowViewProps) {
const { Text } = jest.requireActual('react-native');
+
+ mockQuoteRowRenderCounts.set(
+ quoteRequestId,
+ (mockQuoteRowRenderCounts.get(quoteRequestId) ?? 0) + 1,
+ );
+
return (
{providerName} - {quoteRequestId}
);
- },
-}));
+ }
+
+ return {
+ QuoteRowView: ReactActual.memo(MockQuoteRowView),
+ };
+});
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
@@ -50,6 +67,7 @@ describe('QuoteList', () => {
beforeEach(() => {
jest.clearAllMocks();
+ mockQuoteRowRenderCounts.clear();
});
describe('rendering', () => {
@@ -126,6 +144,50 @@ describe('QuoteList', () => {
});
});
+ describe('memoization', () => {
+ it('does not re-render unchanged rows when one quote changes', () => {
+ // Arrange
+ const quotes = [
+ createMockQuote({
+ provider: { name: 'Lifi' },
+ quoteRequestId: 'quote-1',
+ }),
+ createMockQuote({
+ provider: { name: 'Socket' },
+ quoteRequestId: 'quote-2',
+ }),
+ createMockQuote({
+ provider: { name: 'Hop' },
+ quoteRequestId: 'quote-3',
+ }),
+ ];
+ const { rerender } = render();
+ const updatedQuotes = [
+ createMockQuote({
+ provider: { name: 'Lifi' },
+ quoteRequestId: 'quote-1',
+ }),
+ createMockQuote({
+ provider: { name: 'Socket' },
+ quoteRequestId: 'quote-2',
+ formattedTotalCost: '$101.00',
+ }),
+ createMockQuote({
+ provider: { name: 'Hop' },
+ quoteRequestId: 'quote-3',
+ }),
+ ];
+
+ // Act
+ rerender();
+
+ // Assert
+ expect(mockQuoteRowRenderCounts.get('quote-1')).toBe(1);
+ expect(mockQuoteRowRenderCounts.get('quote-2')).toBe(2);
+ expect(mockQuoteRowRenderCounts.get('quote-3')).toBe(1);
+ });
+ });
+
describe('prop passing', () => {
it('passes all props to QuoteRow components', () => {
const quote = createMockQuote({