diff --git a/packages/harmony/src/components/button/FilterButton/FilterButton.tsx b/packages/harmony/src/components/button/FilterButton/FilterButton.tsx index f21b6a14217..8fde04e6cf8 100644 --- a/packages/harmony/src/components/button/FilterButton/FilterButton.tsx +++ b/packages/harmony/src/components/button/FilterButton/FilterButton.tsx @@ -5,7 +5,9 @@ import { useCallback, useEffect, useMemo, - Ref + Ref, + lazy, + Suspense } from 'react' import { CSSObject, useTheme } from '@emotion/react' @@ -20,9 +22,27 @@ import { Text } from '~harmony/components/text/Text' import { useControlled } from '~harmony/hooks/useControlled' import { IconCaretDown, IconCloseAlt, IconSearch } from '~harmony/icons' -import { OptionsList, VirtualizedOptionsList } from './FilterButtonOptionsList' +import { OptionsList } from './FilterButtonOptionsList' import { FilterButtonProps } from './types' +/** + * `react-virtualized` is ~638 KB of source and is only needed when a consumer + * opts in with the `virtualized` prop (3 call sites across the web app). It was + * previously a static import, so every surface using FilterButton — and + * FilterButton is reachable from the eager app shell via PaymentMethod — paid + * for it. Loaded on demand instead; the menu is only rendered while open, so + * the import starts when a virtualized filter menu is actually opened. + */ +const VirtualizedOptionsList = lazy(() => + import('./FilterButtonVirtualizedOptionsList').then((m) => ({ + // `lazy()` erases the component's `` generic, so it + // resolves to the `string` instantiation. Re-declaring the generic on the + // lazy const keeps call sites type-safe; the generic is erased at runtime + // either way, so this only restores what lazy() dropped. + default: m.VirtualizedOptionsList as typeof m.VirtualizedOptionsList + })) +) as typeof import('./FilterButtonVirtualizedOptionsList').VirtualizedOptionsList + const messages = { noMatches: 'No matches' } @@ -228,13 +248,15 @@ export const FilterButton = forwardRef(function FilterButton< const optionElements = filteredOptions ? ( virtualized ? ( - + + + ) : ( = { onChange: (value: Value) => void } -type VirtualizedOptionsListProps = { - options: FilterButtonOptionType[] - optionRefs: RefObject - onChange: (value: Value) => void - height: CSSObject['height'] - width: CSSObject['width'] -} - -export const VirtualizedOptionsList = ({ - options, - optionRefs, - onChange, - height = 100, - width = 100 -}: VirtualizedOptionsListProps) => { - const [rowHeight, setRowHeight] = useState(50) - useEffect(() => { - if (optionRefs.current?.[0]) { - setRowHeight(optionRefs.current?.[0].offsetHeight) - } - }, [optionRefs]) - - const renderItem = useCallback( - ({ index, style }: ListRowProps) => { - const option = options[index] - - return ( - { - if (optionRefs && optionRefs.current && el) { - optionRefs.current[index] = el - } - }} - key={option.value} - {...option} - onChange={onChange} - /> - ) - }, - [onChange, optionRefs, options] - ) - - return ( - - ) -} - export const OptionsList = ({ options, isOpen, diff --git a/packages/harmony/src/components/button/FilterButton/FilterButtonVirtualizedOptionsList.tsx b/packages/harmony/src/components/button/FilterButton/FilterButtonVirtualizedOptionsList.tsx new file mode 100644 index 00000000000..51efac4b450 --- /dev/null +++ b/packages/harmony/src/components/button/FilterButton/FilterButtonVirtualizedOptionsList.tsx @@ -0,0 +1,63 @@ +import { useState, useCallback, useEffect, RefObject } from 'react' + +import { CSSObject } from '@emotion/react' +import { List, ListRowProps } from 'react-virtualized' + +import { MenuItem } from '~harmony/components/internal/MenuItem' + +import { FilterButtonOptionType } from './types' + +type VirtualizedOptionsListProps = { + options: FilterButtonOptionType[] + optionRefs: RefObject + onChange: (value: Value) => void + height: CSSObject['height'] + width: CSSObject['width'] +} + +export const VirtualizedOptionsList = ({ + options, + optionRefs, + onChange, + height = 100, + width = 100 +}: VirtualizedOptionsListProps) => { + const [rowHeight, setRowHeight] = useState(50) + useEffect(() => { + if (optionRefs.current?.[0]) { + setRowHeight(optionRefs.current?.[0].offsetHeight) + } + }, [optionRefs]) + + const renderItem = useCallback( + ({ index, style }: ListRowProps) => { + const option = options[index] + + return ( + { + if (optionRefs && optionRefs.current && el) { + optionRefs.current[index] = el + } + }} + key={option.value} + {...option} + onChange={onChange} + /> + ) + }, + [onChange, optionRefs, options] + ) + + return ( + + ) +} diff --git a/packages/web/src/components/audio-transactions-table/AudioTransactionsTable.tsx b/packages/web/src/components/audio-transactions-table/AudioTransactionsTable.tsx index 3e44f2f3594..61fc81ef1a6 100644 --- a/packages/web/src/components/audio-transactions-table/AudioTransactionsTable.tsx +++ b/packages/web/src/components/audio-transactions-table/AudioTransactionsTable.tsx @@ -61,12 +61,7 @@ const defaultColumns: AudioTransactionsTableColumn[] = [ 'spacer2' ] -export const isChangePositive = (tx: TransactionDetails) => { - return ( - tx.transactionType === TransactionType.PURCHASE || - tx.method === TransactionMethod.RECEIVE - ) -} +export { isChangePositive } from './isChangePositive' // Cell Render Functions const renderTransactionTypeCell = (cellInfo: TransactionCell) => { diff --git a/packages/web/src/components/audio-transactions-table/isChangePositive.ts b/packages/web/src/components/audio-transactions-table/isChangePositive.ts new file mode 100644 index 00000000000..5eb20d22ef5 --- /dev/null +++ b/packages/web/src/components/audio-transactions-table/isChangePositive.ts @@ -0,0 +1,21 @@ +import { + TransactionDetails, + TransactionMethod, + TransactionType +} from '@audius/common/store' + +/** + * Whether a transaction increases the user's balance. + * + * Lives in its own module rather than in `AudioTransactionsTable` so that + * consumers needing only this predicate don't pull in the table — which imports + * `components/table` and, through it, `react-virtualized` (~638 KB of source). + * `TransactionDetailsContent` is reachable from the eagerly-registered + * TransactionDetails modal, so that edge landed the whole table in the entry chunk. + */ +export const isChangePositive = (tx: TransactionDetails) => { + return ( + tx.transactionType === TransactionType.PURCHASE || + tx.method === TransactionMethod.RECEIVE + ) +} diff --git a/packages/web/src/components/transaction-details-modal/components/TransactionDetailsContent.tsx b/packages/web/src/components/transaction-details-modal/components/TransactionDetailsContent.tsx index e34f5db4aa3..c219fdf95e5 100644 --- a/packages/web/src/components/transaction-details-modal/components/TransactionDetailsContent.tsx +++ b/packages/web/src/components/transaction-details-modal/components/TransactionDetailsContent.tsx @@ -21,7 +21,7 @@ import { import cn from 'classnames' import { AudioTransactionIcon } from 'components/audio-transaction-icon' -import { isChangePositive } from 'components/audio-transactions-table/AudioTransactionsTable' +import { isChangePositive } from 'components/audio-transactions-table/isChangePositive' import LoadingSpinner from 'components/loading-spinner/LoadingSpinner' import { getChallengeConfig } from 'pages/rewards-page/config'