From 6dcbcb1fed3c2eb1b6cc7cb8726d8f43470f2be7 Mon Sep 17 00:00:00 2001 From: Camillebzd Date: Thu, 8 Jan 2026 13:27:07 +0100 Subject: [PATCH 1/8] added vertical scroll update on the swap and transfer component to manage stop on hovering --- .../swap-transfer-tracker/index.tsx | 62 +++++++++--- .../swap-transfer-tracker/swaps.tsx | 91 ++++++++++++++++-- .../swap-transfer-tracker/transfers.tsx | 94 ++++++++++++++++--- frontend/hooks/use-transfer-events.ts | 10 +- frontend/hooks/use-vertical-scroll.ts | 68 ++++++++++++++ 5 files changed, 292 insertions(+), 33 deletions(-) create mode 100644 frontend/hooks/use-vertical-scroll.ts diff --git a/frontend/components/swap-transfer-tracker/index.tsx b/frontend/components/swap-transfer-tracker/index.tsx index a01b0ec..ca7639d 100644 --- a/frontend/components/swap-transfer-tracker/index.tsx +++ b/frontend/components/swap-transfer-tracker/index.tsx @@ -1,6 +1,7 @@ 'use client' -import { ArrowLeftRight, Send } from 'lucide-react' +import { ArrowLeftRight, Pause, Play, Send } from 'lucide-react' +import { useState } from 'react' import { LiveBadge } from '@/components/common/live-badge' import { SectionHeader } from '@/components/ui/section-header' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' @@ -26,13 +27,34 @@ export default function SwapTransferTracker() { isConnected: isTransferConnected, cumulativeTransferred, } = useTransferEvents() + const [isFollowingChain, setIsFollowingChain] = useState(true) + const [isHovering, setIsHovering] = useState(false) + const isPaused = !isFollowingChain || isHovering return (
+ > + +
@@ -52,22 +74,38 @@ export default function SwapTransferTracker() {
- - + + - +
diff --git a/frontend/components/swap-transfer-tracker/swaps.tsx b/frontend/components/swap-transfer-tracker/swaps.tsx index 27f0937..4185d8e 100644 --- a/frontend/components/swap-transfer-tracker/swaps.tsx +++ b/frontend/components/swap-transfer-tracker/swaps.tsx @@ -1,6 +1,8 @@ 'use client' -import { AnimatePresence } from 'framer-motion' +import { useEffect, useMemo, useRef, useState } from 'react' +import { List, type RowComponentProps } from 'react-window' +import { useVerticalScroll } from '@/hooks/use-vertical-scroll' import { cn } from '@/lib/utils' import type { SwapData } from '@/types/swap' import { SwapRow } from './swap-row' @@ -8,11 +10,76 @@ import { SwapRow } from './swap-row' interface SwapsProps { data: SwapData[] isLoading: boolean + isFollowing: boolean } const TABLE_GRID = 'grid grid-cols-6 gap-6 px-4' +const ROW_HEIGHT = 45 + +interface SwapRowData { + swaps: SwapData[] + gridClass: string +} + +function SwapCell({ + index, + style, + swaps, + gridClass, +}: RowComponentProps) { + const swap = swaps[index] + + return ( +
+ +
+ ) +} + +export function Swaps({ data, isLoading, isFollowing }: SwapsProps) { + const containerRef = useRef(null) + const [containerHeight, setContainerHeight] = useState(384) + + // Reverse swaps so newest items are at the bottom + const reversedSwaps = useMemo(() => [...data].reverse(), [data]) + + const { listRef } = useVerticalScroll({ + items: reversedSwaps, + isFollowing, + }) + + const rowProps = useMemo( + () => ({ swaps: reversedSwaps, gridClass: TABLE_GRID }), + [reversedSwaps], + ) + + useEffect(() => { + const node = containerRef.current + if (!node) return + + const updateHeight = () => { + setContainerHeight(node.clientHeight) + } + updateHeight() + + const resizeObserver = new ResizeObserver(updateHeight) + resizeObserver.observe(node) + return () => resizeObserver.disconnect() + }, []) + + // Prevent scroll with non-passive event listener when following + useEffect(() => { + const node = containerRef.current + if (!node || !isFollowing) return + + const preventScroll = (e: WheelEvent) => { + e.preventDefault() + } + + node.addEventListener('wheel', preventScroll, { passive: false }) + return () => node.removeEventListener('wheel', preventScroll) + }, [isFollowing]) -export function Swaps({ data, isLoading }: SwapsProps) { return (
Time
-
+
{data.length === 0 ? (

@@ -37,11 +104,19 @@ export function Swaps({ data, isLoading }: SwapsProps) {

) : ( - - {data.map((swap) => ( - - ))} - + )}
diff --git a/frontend/components/swap-transfer-tracker/transfers.tsx b/frontend/components/swap-transfer-tracker/transfers.tsx index 493b460..81db7fe 100644 --- a/frontend/components/swap-transfer-tracker/transfers.tsx +++ b/frontend/components/swap-transfer-tracker/transfers.tsx @@ -1,6 +1,8 @@ 'use client' -import { AnimatePresence } from 'framer-motion' +import { useEffect, useMemo, useRef, useState } from 'react' +import { List, type RowComponentProps } from 'react-window' +import { useVerticalScroll } from '@/hooks/use-vertical-scroll' import { cn } from '@/lib/utils' import type { TransferData } from '@/types/transfer' import { CumulativeTransferCounter } from './cumulative-transfer-counter' @@ -10,15 +12,81 @@ interface TransfersProps { transfers: TransferData[] isLoading: boolean cumulativeTransferred: bigint + isFollowing: boolean } const TABLE_GRID = 'grid grid-cols-6 gap-6 px-4' +const ROW_HEIGHT = 45 + +interface TransferRowData { + transfers: TransferData[] + gridClass: string +} + +function TransferCell({ + index, + style, + transfers, + gridClass, +}: RowComponentProps) { + const transfer = transfers[index] + + return ( +
+ +
+ ) +} export function Transfers({ transfers, isLoading, cumulativeTransferred, + isFollowing, }: TransfersProps) { + const containerRef = useRef(null) + const [containerHeight, setContainerHeight] = useState(384) + + // Reverse transfers so newest items are at the bottom + const reversedTransfers = useMemo(() => [...transfers].reverse(), [transfers]) + + const { listRef } = useVerticalScroll({ + items: reversedTransfers, + isFollowing, + }) + + const rowProps = useMemo( + () => ({ transfers: reversedTransfers, gridClass: TABLE_GRID }), + [reversedTransfers], + ) + + useEffect(() => { + const node = containerRef.current + if (!node) return + + const updateHeight = () => { + setContainerHeight(node.clientHeight) + } + updateHeight() + + const resizeObserver = new ResizeObserver(updateHeight) + resizeObserver.observe(node) + return () => resizeObserver.disconnect() + }, []) + + // Prevent scroll with non-passive event listener when following + useEffect(() => { + const node = containerRef.current + if (!node || !isFollowing) return + + const preventScroll = (e: WheelEvent) => { + e.preventDefault() + } + + node.addEventListener('wheel', preventScroll, { passive: false }) + return () => node.removeEventListener('wheel', preventScroll) + }, [isFollowing]) + return (
Time
-
+
{transfers.length === 0 ? (

@@ -47,15 +115,19 @@ export function Transfers({

) : ( - - {transfers.map((transfer) => ( - - ))} - + )}
diff --git a/frontend/hooks/use-transfer-events.ts b/frontend/hooks/use-transfer-events.ts index c50dd0f..950f107 100644 --- a/frontend/hooks/use-transfer-events.ts +++ b/frontend/hooks/use-transfer-events.ts @@ -12,7 +12,8 @@ import type { SerializableEventData } from '@/types/events' import type { TransferData } from '@/types/transfer' import { parseTopicsString } from '@/utils/abi-decode' -const MAX_TRANSFERS = 50 +// Uses -1 to indicate no limit on number of transfers stored +const MAX_TRANSFERS = -1 /** * Parse native transfer event (TxnCallFrame with non-zero value) @@ -132,7 +133,12 @@ export function useTransferEvents() { if (!transferData) return - setAllTransfers((prev) => [transferData, ...prev].slice(0, MAX_TRANSFERS)) + setAllTransfers((prev) => { + if (MAX_TRANSFERS === -1) { + return [transferData, ...prev] + } + return [transferData, ...prev].slice(0, MAX_TRANSFERS) + }) // Update cumulative total setCumulativeTransferred((prev) => prev + BigInt(transferData.value)) diff --git a/frontend/hooks/use-vertical-scroll.ts b/frontend/hooks/use-vertical-scroll.ts new file mode 100644 index 0000000..c8e826e --- /dev/null +++ b/frontend/hooks/use-vertical-scroll.ts @@ -0,0 +1,68 @@ +'use client' + +import { useEffect, useLayoutEffect, useRef } from 'react' +import type { ListImperativeAPI } from 'react-window' + +interface UseTransfersScrollOptions { + items: T[] + isFollowing: boolean +} + +export function useVerticalScroll({ + items, + isFollowing, +}: UseTransfersScrollOptions) { + const listRef = useRef(null) + const prevItemsLengthRef = useRef(0) + const wasHiddenRef = useRef(false) + + // Handle new items being added + useLayoutEffect(() => { + if (!listRef.current || items.length === 0) return + + const prevLength = prevItemsLengthRef.current + const hasNewItems = prevLength === 0 || items.length > prevLength + + if (hasNewItems && isFollowing) { + // When following, auto-scroll to the bottom (newest items) + requestAnimationFrame(() => { + listRef.current?.scrollToRow({ + index: items.length - 1, + align: 'end', + behavior: 'smooth', + }) + }) + } + + prevItemsLengthRef.current = items.length + }, [items.length, isFollowing]) + + // Handle tab visibility changes - scroll to top when tab becomes visible + useEffect(() => { + const handleVisibilityChange = () => { + if (document.visibilityState === 'hidden') { + wasHiddenRef.current = true + } else if ( + document.visibilityState === 'visible' && + wasHiddenRef.current && + isFollowing && + items.length > 0 + ) { + // Tab became visible after being hidden - scroll to bottom immediately + listRef.current?.scrollToRow({ + index: items.length - 1, + align: 'end', + behavior: 'auto', + }) + wasHiddenRef.current = false + } + } + + document.addEventListener('visibilitychange', handleVisibilityChange) + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange) + } + }, [isFollowing, items.length]) + + return { listRef } +} From 21a9733eb8e6c01af80bb3968d36a90e7f70ef4c Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Thu, 8 Jan 2026 19:21:49 +0530 Subject: [PATCH 2/8] fix(frontend): fix SwapRow and TransferRow scroll behavior (#41) --- .../swap-transfer-tracker/swap-row.tsx | 17 +++-- .../swap-transfer-tracker/swaps.tsx | 65 ++++++++++++------ .../swap-transfer-tracker/transfer-row.tsx | 17 +++-- .../swap-transfer-tracker/transfers.tsx | 65 ++++++++++++------ frontend/hooks/use-vertical-scroll.ts | 68 ------------------- 5 files changed, 104 insertions(+), 128 deletions(-) delete mode 100644 frontend/hooks/use-vertical-scroll.ts diff --git a/frontend/components/swap-transfer-tracker/swap-row.tsx b/frontend/components/swap-transfer-tracker/swap-row.tsx index 9869291..a77afb9 100644 --- a/frontend/components/swap-transfer-tracker/swap-row.tsx +++ b/frontend/components/swap-transfer-tracker/swap-row.tsx @@ -1,6 +1,6 @@ 'use client' -import { motion } from 'framer-motion' +import { memo } from 'react' import { ExternalLink } from '@/components/ui/external-link' import { EXPLORER_URL } from '@/constants/common' import { getSwapProviderConfig } from '@/constants/swap-provider-config' @@ -15,15 +15,14 @@ interface SwapRowProps { gridClass: string } -export function SwapRow({ swap, gridClass }: SwapRowProps) { +// Memoized to prevent re-renders when parent re-renders +// Note: motion.div removed because it conflicts with react-window virtualization +// (rows mount/unmount frequently as they scroll in/out of view) +export const SwapRow = memo(function SwapRow({ swap, gridClass }: SwapRowProps) { const config = getSwapProviderConfig(swap.provider) return ( - {formatTimeDisplay(swap.timestamp)} - +
) -} +}) diff --git a/frontend/components/swap-transfer-tracker/swaps.tsx b/frontend/components/swap-transfer-tracker/swaps.tsx index 4185d8e..5550d2b 100644 --- a/frontend/components/swap-transfer-tracker/swaps.tsx +++ b/frontend/components/swap-transfer-tracker/swaps.tsx @@ -1,8 +1,8 @@ 'use client' -import { useEffect, useMemo, useRef, useState } from 'react' -import { List, type RowComponentProps } from 'react-window' -import { useVerticalScroll } from '@/hooks/use-vertical-scroll' +import type { ReactElement } from 'react' +import { useEffect, useRef, useState } from 'react' +import { List, type ListImperativeAPI, type RowComponentProps } from 'react-window' import { cn } from '@/lib/utils' import type { SwapData } from '@/types/swap' import { SwapRow } from './swap-row' @@ -17,41 +17,64 @@ const TABLE_GRID = 'grid grid-cols-6 gap-6 px-4' const ROW_HEIGHT = 45 interface SwapRowData { - swaps: SwapData[] + swapsRef: React.RefObject gridClass: string } +// Cell component defined outside to maintain stable reference function SwapCell({ index, style, - swaps, + swapsRef, gridClass, -}: RowComponentProps) { - const swap = swaps[index] +}: RowComponentProps): ReactElement { + const swap = swapsRef.current?.[index] return (
- + {swap && }
) } +// Stable rowProps object +const STABLE_GRID_CLASS = TABLE_GRID + export function Swaps({ data, isLoading, isFollowing }: SwapsProps) { const containerRef = useRef(null) + const listRef = useRef(null) const [containerHeight, setContainerHeight] = useState(384) - // Reverse swaps so newest items are at the bottom - const reversedSwaps = useMemo(() => [...data].reverse(), [data]) + // Freeze data when paused - this prevents ALL re-renders while paused + const [displayedData, setDisplayedData] = useState(data) + const wasFollowingRef = useRef(isFollowing) - const { listRef } = useVerticalScroll({ - items: reversedSwaps, - isFollowing, - }) + // Update displayed data only when following, or when resuming from pause + useEffect(() => { + if (isFollowing) { + setDisplayedData(data) + } + // If we just resumed from pause, update to latest + if (isFollowing && !wasFollowingRef.current) { + setDisplayedData(data) + } + wasFollowingRef.current = isFollowing + }, [data, isFollowing]) - const rowProps = useMemo( - () => ({ swaps: reversedSwaps, gridClass: TABLE_GRID }), - [reversedSwaps], - ) + // Store displayed data in ref for stable rowProps + const swapsRef = useRef(displayedData) + swapsRef.current = displayedData + + // Auto-scroll to top when following and new data arrives + useEffect(() => { + if (isFollowing && displayedData.length > 0 && listRef.current) { + listRef.current.scrollToRow({ index: 0, align: 'start', behavior: 'auto' }) + } + }, [displayedData.length, isFollowing]) + + // Stable rowProps - swapsRef never changes reference, only its .current + const rowPropsRef = useRef({ swapsRef, gridClass: STABLE_GRID_CLASS }) + const rowProps = rowPropsRef.current useEffect(() => { const node = containerRef.current @@ -67,7 +90,7 @@ export function Swaps({ data, isLoading, isFollowing }: SwapsProps) { return () => resizeObserver.disconnect() }, []) - // Prevent scroll with non-passive event listener when following + // Prevent scroll when following useEffect(() => { const node = containerRef.current if (!node || !isFollowing) return @@ -97,7 +120,7 @@ export function Swaps({ data, isLoading, isFollowing }: SwapsProps) {
- {data.length === 0 ? ( + {displayedData.length === 0 ? (

{isLoading ? 'Waiting for events...' : 'No swaps yet'} @@ -107,7 +130,7 @@ export function Swaps({ data, isLoading, isFollowing }: SwapsProps) { {formatTimeDisplay(transfer.timestamp)} - +

) -} +}) diff --git a/frontend/components/swap-transfer-tracker/transfers.tsx b/frontend/components/swap-transfer-tracker/transfers.tsx index 81db7fe..cf9442a 100644 --- a/frontend/components/swap-transfer-tracker/transfers.tsx +++ b/frontend/components/swap-transfer-tracker/transfers.tsx @@ -1,8 +1,8 @@ 'use client' -import { useEffect, useMemo, useRef, useState } from 'react' -import { List, type RowComponentProps } from 'react-window' -import { useVerticalScroll } from '@/hooks/use-vertical-scroll' +import type { ReactElement } from 'react' +import { useEffect, useRef, useState } from 'react' +import { List, type ListImperativeAPI, type RowComponentProps } from 'react-window' import { cn } from '@/lib/utils' import type { TransferData } from '@/types/transfer' import { CumulativeTransferCounter } from './cumulative-transfer-counter' @@ -19,25 +19,29 @@ const TABLE_GRID = 'grid grid-cols-6 gap-6 px-4' const ROW_HEIGHT = 45 interface TransferRowData { - transfers: TransferData[] + transfersRef: React.RefObject gridClass: string } +// Cell component defined outside to maintain stable reference function TransferCell({ index, style, - transfers, + transfersRef, gridClass, -}: RowComponentProps) { - const transfer = transfers[index] +}: RowComponentProps): ReactElement { + const transfer = transfersRef.current?.[index] return (
- + {transfer && }
) } +// Stable rowProps object +const STABLE_GRID_CLASS = TABLE_GRID + export function Transfers({ transfers, isLoading, @@ -45,20 +49,39 @@ export function Transfers({ isFollowing, }: TransfersProps) { const containerRef = useRef(null) + const listRef = useRef(null) const [containerHeight, setContainerHeight] = useState(384) - // Reverse transfers so newest items are at the bottom - const reversedTransfers = useMemo(() => [...transfers].reverse(), [transfers]) + // Freeze data when paused - this prevents ALL re-renders while paused + const [displayedData, setDisplayedData] = useState(transfers) + const wasFollowingRef = useRef(isFollowing) - const { listRef } = useVerticalScroll({ - items: reversedTransfers, - isFollowing, - }) + // Update displayed data only when following, or when resuming from pause + useEffect(() => { + if (isFollowing) { + setDisplayedData(transfers) + } + // If we just resumed from pause, update to latest + if (isFollowing && !wasFollowingRef.current) { + setDisplayedData(transfers) + } + wasFollowingRef.current = isFollowing + }, [transfers, isFollowing]) - const rowProps = useMemo( - () => ({ transfers: reversedTransfers, gridClass: TABLE_GRID }), - [reversedTransfers], - ) + // Store displayed data in ref for stable rowProps + const transfersRef = useRef(displayedData) + transfersRef.current = displayedData + + // Auto-scroll to top when following and new data arrives + useEffect(() => { + if (isFollowing && displayedData.length > 0 && listRef.current) { + listRef.current.scrollToRow({ index: 0, align: 'start', behavior: 'auto' }) + } + }, [displayedData.length, isFollowing]) + + // Stable rowProps - transfersRef never changes reference, only its .current + const rowPropsRef = useRef({ transfersRef, gridClass: STABLE_GRID_CLASS }) + const rowProps = rowPropsRef.current useEffect(() => { const node = containerRef.current @@ -74,7 +97,7 @@ export function Transfers({ return () => resizeObserver.disconnect() }, []) - // Prevent scroll with non-passive event listener when following + // Prevent scroll when following useEffect(() => { const node = containerRef.current if (!node || !isFollowing) return @@ -108,7 +131,7 @@ export function Transfers({
- {transfers.length === 0 ? ( + {displayedData.length === 0 ? (

{isLoading ? 'Waiting for events...' : 'No transfers yet'} @@ -118,7 +141,7 @@ export function Transfers({ { - items: T[] - isFollowing: boolean -} - -export function useVerticalScroll({ - items, - isFollowing, -}: UseTransfersScrollOptions) { - const listRef = useRef(null) - const prevItemsLengthRef = useRef(0) - const wasHiddenRef = useRef(false) - - // Handle new items being added - useLayoutEffect(() => { - if (!listRef.current || items.length === 0) return - - const prevLength = prevItemsLengthRef.current - const hasNewItems = prevLength === 0 || items.length > prevLength - - if (hasNewItems && isFollowing) { - // When following, auto-scroll to the bottom (newest items) - requestAnimationFrame(() => { - listRef.current?.scrollToRow({ - index: items.length - 1, - align: 'end', - behavior: 'smooth', - }) - }) - } - - prevItemsLengthRef.current = items.length - }, [items.length, isFollowing]) - - // Handle tab visibility changes - scroll to top when tab becomes visible - useEffect(() => { - const handleVisibilityChange = () => { - if (document.visibilityState === 'hidden') { - wasHiddenRef.current = true - } else if ( - document.visibilityState === 'visible' && - wasHiddenRef.current && - isFollowing && - items.length > 0 - ) { - // Tab became visible after being hidden - scroll to bottom immediately - listRef.current?.scrollToRow({ - index: items.length - 1, - align: 'end', - behavior: 'auto', - }) - wasHiddenRef.current = false - } - } - - document.addEventListener('visibilitychange', handleVisibilityChange) - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange) - } - }, [isFollowing, items.length]) - - return { listRef } -} From 79d00cfb32f3d1c27436cfa97ed1815732d16a6a Mon Sep 17 00:00:00 2001 From: Camillebzd Date: Thu, 8 Jan 2026 15:24:34 +0100 Subject: [PATCH 3/8] created use-virtualized-list custom hook to refacto code between swap and transfer tabs --- .../swap-transfer-tracker/index.tsx | 6 +- .../swap-transfer-tracker/swaps.tsx | 80 ++------------ .../swap-transfer-tracker/transfers.tsx | 80 ++------------ frontend/components/ui/stat-card.tsx | 6 +- frontend/hooks/use-virtualized-list.ts | 101 ++++++++++++++++++ 5 files changed, 131 insertions(+), 142 deletions(-) create mode 100644 frontend/hooks/use-virtualized-list.ts diff --git a/frontend/components/swap-transfer-tracker/index.tsx b/frontend/components/swap-transfer-tracker/index.tsx index ca7639d..7344f6a 100644 --- a/frontend/components/swap-transfer-tracker/index.tsx +++ b/frontend/components/swap-transfer-tracker/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { ArrowLeftRight, Pause, Play, Send } from 'lucide-react' +import { ArrowLeftRight, Info, Pause, Play, Send } from 'lucide-react' import { useState } from 'react' import { LiveBadge } from '@/components/common/live-badge' import { SectionHeader } from '@/components/ui/section-header' @@ -109,6 +109,10 @@ export default function SwapTransferTracker() {

+
+ + Hover to pause +
) } diff --git a/frontend/components/swap-transfer-tracker/swaps.tsx b/frontend/components/swap-transfer-tracker/swaps.tsx index 5550d2b..f84d85e 100644 --- a/frontend/components/swap-transfer-tracker/swaps.tsx +++ b/frontend/components/swap-transfer-tracker/swaps.tsx @@ -1,8 +1,8 @@ 'use client' import type { ReactElement } from 'react' -import { useEffect, useRef, useState } from 'react' -import { List, type ListImperativeAPI, type RowComponentProps } from 'react-window' +import { List, type RowComponentProps } from 'react-window' +import { useVirtualizedList } from '@/hooks/use-virtualized-list' import { cn } from '@/lib/utils' import type { SwapData } from '@/types/swap' import { SwapRow } from './swap-row' @@ -17,7 +17,7 @@ const TABLE_GRID = 'grid grid-cols-6 gap-6 px-4' const ROW_HEIGHT = 45 interface SwapRowData { - swapsRef: React.RefObject + dataRef: React.RefObject gridClass: string } @@ -25,10 +25,10 @@ interface SwapRowData { function SwapCell({ index, style, - swapsRef, + dataRef, gridClass, }: RowComponentProps): ReactElement { - const swap = swapsRef.current?.[index] + const swap = dataRef.current?.[index] return (
@@ -37,71 +37,13 @@ function SwapCell({ ) } -// Stable rowProps object -const STABLE_GRID_CLASS = TABLE_GRID - export function Swaps({ data, isLoading, isFollowing }: SwapsProps) { - const containerRef = useRef(null) - const listRef = useRef(null) - const [containerHeight, setContainerHeight] = useState(384) - - // Freeze data when paused - this prevents ALL re-renders while paused - const [displayedData, setDisplayedData] = useState(data) - const wasFollowingRef = useRef(isFollowing) - - // Update displayed data only when following, or when resuming from pause - useEffect(() => { - if (isFollowing) { - setDisplayedData(data) - } - // If we just resumed from pause, update to latest - if (isFollowing && !wasFollowingRef.current) { - setDisplayedData(data) - } - wasFollowingRef.current = isFollowing - }, [data, isFollowing]) - - // Store displayed data in ref for stable rowProps - const swapsRef = useRef(displayedData) - swapsRef.current = displayedData - - // Auto-scroll to top when following and new data arrives - useEffect(() => { - if (isFollowing && displayedData.length > 0 && listRef.current) { - listRef.current.scrollToRow({ index: 0, align: 'start', behavior: 'auto' }) - } - }, [displayedData.length, isFollowing]) - - // Stable rowProps - swapsRef never changes reference, only its .current - const rowPropsRef = useRef({ swapsRef, gridClass: STABLE_GRID_CLASS }) - const rowProps = rowPropsRef.current - - useEffect(() => { - const node = containerRef.current - if (!node) return - - const updateHeight = () => { - setContainerHeight(node.clientHeight) - } - updateHeight() - - const resizeObserver = new ResizeObserver(updateHeight) - resizeObserver.observe(node) - return () => resizeObserver.disconnect() - }, []) - - // Prevent scroll when following - useEffect(() => { - const node = containerRef.current - if (!node || !isFollowing) return - - const preventScroll = (e: WheelEvent) => { - e.preventDefault() - } - - node.addEventListener('wheel', preventScroll, { passive: false }) - return () => node.removeEventListener('wheel', preventScroll) - }, [isFollowing]) + const { containerRef, listRef, containerHeight, displayedData, rowProps } = + useVirtualizedList({ + data, + isFollowing, + gridClass: TABLE_GRID, + }) return (
diff --git a/frontend/components/swap-transfer-tracker/transfers.tsx b/frontend/components/swap-transfer-tracker/transfers.tsx index cf9442a..cf4b4de 100644 --- a/frontend/components/swap-transfer-tracker/transfers.tsx +++ b/frontend/components/swap-transfer-tracker/transfers.tsx @@ -1,8 +1,8 @@ 'use client' import type { ReactElement } from 'react' -import { useEffect, useRef, useState } from 'react' -import { List, type ListImperativeAPI, type RowComponentProps } from 'react-window' +import { List, type RowComponentProps } from 'react-window' +import { useVirtualizedList } from '@/hooks/use-virtualized-list' import { cn } from '@/lib/utils' import type { TransferData } from '@/types/transfer' import { CumulativeTransferCounter } from './cumulative-transfer-counter' @@ -19,7 +19,7 @@ const TABLE_GRID = 'grid grid-cols-6 gap-6 px-4' const ROW_HEIGHT = 45 interface TransferRowData { - transfersRef: React.RefObject + dataRef: React.RefObject gridClass: string } @@ -27,10 +27,10 @@ interface TransferRowData { function TransferCell({ index, style, - transfersRef, + dataRef, gridClass, }: RowComponentProps): ReactElement { - const transfer = transfersRef.current?.[index] + const transfer = dataRef.current?.[index] return (
@@ -39,76 +39,18 @@ function TransferCell({ ) } -// Stable rowProps object -const STABLE_GRID_CLASS = TABLE_GRID - export function Transfers({ transfers, isLoading, cumulativeTransferred, isFollowing, }: TransfersProps) { - const containerRef = useRef(null) - const listRef = useRef(null) - const [containerHeight, setContainerHeight] = useState(384) - - // Freeze data when paused - this prevents ALL re-renders while paused - const [displayedData, setDisplayedData] = useState(transfers) - const wasFollowingRef = useRef(isFollowing) - - // Update displayed data only when following, or when resuming from pause - useEffect(() => { - if (isFollowing) { - setDisplayedData(transfers) - } - // If we just resumed from pause, update to latest - if (isFollowing && !wasFollowingRef.current) { - setDisplayedData(transfers) - } - wasFollowingRef.current = isFollowing - }, [transfers, isFollowing]) - - // Store displayed data in ref for stable rowProps - const transfersRef = useRef(displayedData) - transfersRef.current = displayedData - - // Auto-scroll to top when following and new data arrives - useEffect(() => { - if (isFollowing && displayedData.length > 0 && listRef.current) { - listRef.current.scrollToRow({ index: 0, align: 'start', behavior: 'auto' }) - } - }, [displayedData.length, isFollowing]) - - // Stable rowProps - transfersRef never changes reference, only its .current - const rowPropsRef = useRef({ transfersRef, gridClass: STABLE_GRID_CLASS }) - const rowProps = rowPropsRef.current - - useEffect(() => { - const node = containerRef.current - if (!node) return - - const updateHeight = () => { - setContainerHeight(node.clientHeight) - } - updateHeight() - - const resizeObserver = new ResizeObserver(updateHeight) - resizeObserver.observe(node) - return () => resizeObserver.disconnect() - }, []) - - // Prevent scroll when following - useEffect(() => { - const node = containerRef.current - if (!node || !isFollowing) return - - const preventScroll = (e: WheelEvent) => { - e.preventDefault() - } - - node.addEventListener('wheel', preventScroll, { passive: false }) - return () => node.removeEventListener('wheel', preventScroll) - }, [isFollowing]) + const { containerRef, listRef, containerHeight, displayedData, rowProps } = + useVirtualizedList({ + data: transfers, + isFollowing, + gridClass: TABLE_GRID, + }) return (
diff --git a/frontend/components/ui/stat-card.tsx b/frontend/components/ui/stat-card.tsx index 859f172..1d897e8 100644 --- a/frontend/components/ui/stat-card.tsx +++ b/frontend/components/ui/stat-card.tsx @@ -24,9 +24,9 @@ export function StatCard({ iconBgClassName = 'bg-tracker-active/10', }: StatCardProps) { return ( -
-
-
+
+
+

{label}

diff --git a/frontend/hooks/use-virtualized-list.ts b/frontend/hooks/use-virtualized-list.ts new file mode 100644 index 0000000..db0b9c7 --- /dev/null +++ b/frontend/hooks/use-virtualized-list.ts @@ -0,0 +1,101 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import type { ListImperativeAPI } from 'react-window' + +interface UseVirtualizedListOptions { + data: T[] + isFollowing: boolean + gridClass: string +} + +interface UseVirtualizedListReturn { + containerRef: React.RefObject + listRef: React.RefObject + containerHeight: number + displayedData: T[] + dataRef: React.RefObject + rowProps: { dataRef: React.RefObject; gridClass: string } +} + +export function useVirtualizedList({ + data, + isFollowing, + gridClass, +}: UseVirtualizedListOptions): UseVirtualizedListReturn { + const containerRef = useRef(null) + const listRef = useRef(null) + const [containerHeight, setContainerHeight] = useState(384) + + // Freeze data when paused - this prevents ALL re-renders while paused + const [displayedData, setDisplayedData] = useState(data) + const wasFollowingRef = useRef(isFollowing) + + // Update displayed data only when following, or when resuming from pause + useEffect(() => { + if (isFollowing) { + setDisplayedData(data) + } + // If we just resumed from pause, update to latest + if (isFollowing && !wasFollowingRef.current) { + setDisplayedData(data) + } + wasFollowingRef.current = isFollowing + }, [data, isFollowing]) + + // Store displayed data in ref for stable rowProps + const dataRef = useRef(displayedData) + dataRef.current = displayedData + + // Auto-scroll to top when following and new data arrives + useEffect(() => { + if (isFollowing && displayedData.length > 0 && listRef.current) { + listRef.current.scrollToRow({ + index: 0, + align: 'start', + behavior: 'auto', + }) + } + }, [displayedData.length, isFollowing]) + + // Stable rowProps - dataRef never changes reference, only its .current + const rowPropsRef = useRef({ dataRef, gridClass }) + const rowProps = rowPropsRef.current + + // Update container height on resize + useEffect(() => { + const node = containerRef.current + if (!node) return + + const updateHeight = () => { + setContainerHeight(node.clientHeight) + } + updateHeight() + + const resizeObserver = new ResizeObserver(updateHeight) + resizeObserver.observe(node) + return () => resizeObserver.disconnect() + }, []) + + // Prevent scroll when following + useEffect(() => { + const node = containerRef.current + if (!node || !isFollowing) return + + const preventScroll = (e: WheelEvent) => { + e.preventDefault() + } + + node.addEventListener('wheel', preventScroll, { passive: false }) + return () => node.removeEventListener('wheel', preventScroll) + }, [isFollowing]) + + return { + containerRef, + listRef, + containerHeight, + displayedData, + dataRef, + rowProps, + } +} From 402c7f91c1c8c71764a149bb39fe6dc69ca8b609 Mon Sep 17 00:00:00 2001 From: Camillebzd Date: Thu, 8 Jan 2026 15:30:15 +0100 Subject: [PATCH 4/8] lint --- frontend/components/swap-transfer-tracker/swap-row.tsx | 5 ++++- frontend/components/swap-transfer-tracker/transfer-row.tsx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/components/swap-transfer-tracker/swap-row.tsx b/frontend/components/swap-transfer-tracker/swap-row.tsx index 042ccc4..c1e83a4 100644 --- a/frontend/components/swap-transfer-tracker/swap-row.tsx +++ b/frontend/components/swap-transfer-tracker/swap-row.tsx @@ -18,7 +18,10 @@ interface SwapRowProps { // Memoized to prevent re-renders when parent re-renders // Note: motion.div removed because it conflicts with react-window virtualization // (rows mount/unmount frequently as they scroll in/out of view) -export const SwapRow = memo(function SwapRow({ swap, gridClass }: SwapRowProps) { +export const SwapRow = memo(function SwapRow({ + swap, + gridClass, +}: SwapRowProps) { const config = getSwapProviderConfig(swap.provider) return ( diff --git a/frontend/components/swap-transfer-tracker/transfer-row.tsx b/frontend/components/swap-transfer-tracker/transfer-row.tsx index c8fc3c9..d789289 100644 --- a/frontend/components/swap-transfer-tracker/transfer-row.tsx +++ b/frontend/components/swap-transfer-tracker/transfer-row.tsx @@ -17,7 +17,10 @@ interface TransferRowProps { // Memoized to prevent re-renders when parent re-renders // Note: motion.div removed because it conflicts with react-window virtualization // (rows mount/unmount frequently as they scroll in/out of view) -export const TransferRow = memo(function TransferRow({ transfer, gridClass }: TransferRowProps) { +export const TransferRow = memo(function TransferRow({ + transfer, + gridClass, +}: TransferRowProps) { const tokenSymbol = transfer.type === 'native' ? 'MON' : 'WMON' return ( From 64188501977574f2d8916c63613d940c202c74e1 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Thu, 8 Jan 2026 21:17:25 +0530 Subject: [PATCH 5/8] refactor(frontend): update SwapRow and TransferRow components to default exports and remove memoization --- .../components/swap-transfer-tracker/swap-row.tsx | 11 ++--------- frontend/components/swap-transfer-tracker/swaps.tsx | 2 +- .../components/swap-transfer-tracker/transfer-row.tsx | 11 ++--------- .../components/swap-transfer-tracker/transfers.tsx | 2 +- 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/frontend/components/swap-transfer-tracker/swap-row.tsx b/frontend/components/swap-transfer-tracker/swap-row.tsx index c1e83a4..d858f64 100644 --- a/frontend/components/swap-transfer-tracker/swap-row.tsx +++ b/frontend/components/swap-transfer-tracker/swap-row.tsx @@ -1,6 +1,5 @@ 'use client' -import { memo } from 'react' import { ExternalLink } from '@/components/ui/external-link' import { EXPLORER_URL } from '@/constants/common' import { getSwapProviderConfig } from '@/constants/swap-provider-config' @@ -15,13 +14,7 @@ interface SwapRowProps { gridClass: string } -// Memoized to prevent re-renders when parent re-renders -// Note: motion.div removed because it conflicts with react-window virtualization -// (rows mount/unmount frequently as they scroll in/out of view) -export const SwapRow = memo(function SwapRow({ - swap, - gridClass, -}: SwapRowProps) { +export default function SwapRow({ swap, gridClass }: SwapRowProps) { const config = getSwapProviderConfig(swap.provider) return ( @@ -80,4 +73,4 @@ export const SwapRow = memo(function SwapRow({

) -}) +} diff --git a/frontend/components/swap-transfer-tracker/swaps.tsx b/frontend/components/swap-transfer-tracker/swaps.tsx index f84d85e..018fbdc 100644 --- a/frontend/components/swap-transfer-tracker/swaps.tsx +++ b/frontend/components/swap-transfer-tracker/swaps.tsx @@ -5,7 +5,7 @@ import { List, type RowComponentProps } from 'react-window' import { useVirtualizedList } from '@/hooks/use-virtualized-list' import { cn } from '@/lib/utils' import type { SwapData } from '@/types/swap' -import { SwapRow } from './swap-row' +import SwapRow from './swap-row' interface SwapsProps { data: SwapData[] diff --git a/frontend/components/swap-transfer-tracker/transfer-row.tsx b/frontend/components/swap-transfer-tracker/transfer-row.tsx index d789289..6927e12 100644 --- a/frontend/components/swap-transfer-tracker/transfer-row.tsx +++ b/frontend/components/swap-transfer-tracker/transfer-row.tsx @@ -1,6 +1,5 @@ 'use client' -import { memo } from 'react' import { ExternalLink } from '@/components/ui/external-link' import { EXPLORER_URL } from '@/constants/common' import { formatTimeDisplay } from '@/lib/timestamp' @@ -14,13 +13,7 @@ interface TransferRowProps { gridClass: string } -// Memoized to prevent re-renders when parent re-renders -// Note: motion.div removed because it conflicts with react-window virtualization -// (rows mount/unmount frequently as they scroll in/out of view) -export const TransferRow = memo(function TransferRow({ - transfer, - gridClass, -}: TransferRowProps) { +export default function TransferRow({ transfer, gridClass }: TransferRowProps) { const tokenSymbol = transfer.type === 'native' ? 'MON' : 'WMON' return ( @@ -65,4 +58,4 @@ export const TransferRow = memo(function TransferRow({
) -}) +} diff --git a/frontend/components/swap-transfer-tracker/transfers.tsx b/frontend/components/swap-transfer-tracker/transfers.tsx index cf4b4de..4fcdf0c 100644 --- a/frontend/components/swap-transfer-tracker/transfers.tsx +++ b/frontend/components/swap-transfer-tracker/transfers.tsx @@ -6,7 +6,7 @@ import { useVirtualizedList } from '@/hooks/use-virtualized-list' import { cn } from '@/lib/utils' import type { TransferData } from '@/types/transfer' import { CumulativeTransferCounter } from './cumulative-transfer-counter' -import { TransferRow } from './transfer-row' +import TransferRow from './transfer-row' interface TransfersProps { transfers: TransferData[] From d844483845e8b9fb112147c230a4eef7cb931a4a Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Thu, 8 Jan 2026 21:25:30 +0530 Subject: [PATCH 6/8] fix(frontend): change scroll behavior to smooth in useVirtualizedList hook --- frontend/hooks/use-virtualized-list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/hooks/use-virtualized-list.ts b/frontend/hooks/use-virtualized-list.ts index db0b9c7..5b2c575 100644 --- a/frontend/hooks/use-virtualized-list.ts +++ b/frontend/hooks/use-virtualized-list.ts @@ -53,7 +53,7 @@ export function useVirtualizedList({ listRef.current.scrollToRow({ index: 0, align: 'start', - behavior: 'auto', + behavior: 'smooth', }) } }, [displayedData.length, isFollowing]) From e62aafed011b73ca663014892a1ec3650a63b8f7 Mon Sep 17 00:00:00 2001 From: Camillebzd Date: Thu, 8 Jan 2026 16:58:34 +0100 Subject: [PATCH 7/8] moved the hovering detection on list itself --- .../swap-transfer-tracker/index.tsx | 48 +++++++------------ .../swap-transfer-tracker/swaps.tsx | 15 ++++-- .../swap-transfer-tracker/transfers.tsx | 15 ++++-- 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/frontend/components/swap-transfer-tracker/index.tsx b/frontend/components/swap-transfer-tracker/index.tsx index 7344f6a..5e3ab0a 100644 --- a/frontend/components/swap-transfer-tracker/index.tsx +++ b/frontend/components/swap-transfer-tracker/index.tsx @@ -27,9 +27,7 @@ export default function SwapTransferTracker() { isConnected: isTransferConnected, cumulativeTransferred, } = useTransferEvents() - const [isFollowingChain, setIsFollowingChain] = useState(true) - const [isHovering, setIsHovering] = useState(false) - const isPaused = !isFollowingChain || isHovering + const [isFollowingData, setIsFollowingData] = useState(true) return (
@@ -39,20 +37,20 @@ export default function SwapTransferTracker() { > @@ -75,37 +73,23 @@ export default function SwapTransferTracker() {
- + - +
diff --git a/frontend/components/swap-transfer-tracker/swaps.tsx b/frontend/components/swap-transfer-tracker/swaps.tsx index 018fbdc..79820c1 100644 --- a/frontend/components/swap-transfer-tracker/swaps.tsx +++ b/frontend/components/swap-transfer-tracker/swaps.tsx @@ -1,6 +1,7 @@ 'use client' import type { ReactElement } from 'react' +import { useState } from 'react' import { List, type RowComponentProps } from 'react-window' import { useVirtualizedList } from '@/hooks/use-virtualized-list' import { cn } from '@/lib/utils' @@ -10,7 +11,7 @@ import SwapRow from './swap-row' interface SwapsProps { data: SwapData[] isLoading: boolean - isFollowing: boolean + isFollowingData: boolean } const TABLE_GRID = 'grid grid-cols-6 gap-6 px-4' @@ -37,7 +38,10 @@ function SwapCell({ ) } -export function Swaps({ data, isLoading, isFollowing }: SwapsProps) { +export function Swaps({ data, isLoading, isFollowingData }: SwapsProps) { + const [isHovering, setIsHovering] = useState(false) + const isFollowing = isFollowingData && !isHovering + const { containerRef, listRef, containerHeight, displayedData, rowProps } = useVirtualizedList({ data, @@ -61,7 +65,12 @@ export function Swaps({ data, isLoading, isFollowing }: SwapsProps) { Time
-
+
setIsHovering(true)} + onPointerLeave={() => setIsHovering(false)} + > {displayedData.length === 0 ? (

diff --git a/frontend/components/swap-transfer-tracker/transfers.tsx b/frontend/components/swap-transfer-tracker/transfers.tsx index 4fcdf0c..5b1e7e1 100644 --- a/frontend/components/swap-transfer-tracker/transfers.tsx +++ b/frontend/components/swap-transfer-tracker/transfers.tsx @@ -1,6 +1,7 @@ 'use client' import type { ReactElement } from 'react' +import { useState } from 'react' import { List, type RowComponentProps } from 'react-window' import { useVirtualizedList } from '@/hooks/use-virtualized-list' import { cn } from '@/lib/utils' @@ -12,7 +13,7 @@ interface TransfersProps { transfers: TransferData[] isLoading: boolean cumulativeTransferred: bigint - isFollowing: boolean + isFollowingData: boolean } const TABLE_GRID = 'grid grid-cols-6 gap-6 px-4' @@ -43,8 +44,11 @@ export function Transfers({ transfers, isLoading, cumulativeTransferred, - isFollowing, + isFollowingData, }: TransfersProps) { + const [isHovering, setIsHovering] = useState(false) + const isFollowing = isFollowingData && !isHovering + const { containerRef, listRef, containerHeight, displayedData, rowProps } = useVirtualizedList({ data: transfers, @@ -72,7 +76,12 @@ export function Transfers({ Time

-
+
setIsHovering(true)} + onPointerLeave={() => setIsHovering(false)} + > {displayedData.length === 0 ? (

From 99de960e4335fcb4437c601174504c627fd12c3f Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Thu, 8 Jan 2026 22:00:03 +0530 Subject: [PATCH 8/8] refactor(frontend): change SwapRow and TransferRow to named exports and update MAX_TRANSFERS to Infinity --- frontend/components/swap-transfer-tracker/swap-row.tsx | 2 +- frontend/components/swap-transfer-tracker/swaps.tsx | 2 +- .../components/swap-transfer-tracker/transfer-row.tsx | 2 +- .../components/swap-transfer-tracker/transfers.tsx | 2 +- frontend/hooks/use-transfer-events.ts | 10 ++-------- frontend/hooks/use-virtualized-list.ts | 10 ++-------- 6 files changed, 8 insertions(+), 20 deletions(-) diff --git a/frontend/components/swap-transfer-tracker/swap-row.tsx b/frontend/components/swap-transfer-tracker/swap-row.tsx index d858f64..58bf958 100644 --- a/frontend/components/swap-transfer-tracker/swap-row.tsx +++ b/frontend/components/swap-transfer-tracker/swap-row.tsx @@ -14,7 +14,7 @@ interface SwapRowProps { gridClass: string } -export default function SwapRow({ swap, gridClass }: SwapRowProps) { +export function SwapRow({ swap, gridClass }: SwapRowProps) { const config = getSwapProviderConfig(swap.provider) return ( diff --git a/frontend/components/swap-transfer-tracker/swaps.tsx b/frontend/components/swap-transfer-tracker/swaps.tsx index 79820c1..38c5064 100644 --- a/frontend/components/swap-transfer-tracker/swaps.tsx +++ b/frontend/components/swap-transfer-tracker/swaps.tsx @@ -6,7 +6,7 @@ import { List, type RowComponentProps } from 'react-window' import { useVirtualizedList } from '@/hooks/use-virtualized-list' import { cn } from '@/lib/utils' import type { SwapData } from '@/types/swap' -import SwapRow from './swap-row' +import { SwapRow } from './swap-row' interface SwapsProps { data: SwapData[] diff --git a/frontend/components/swap-transfer-tracker/transfer-row.tsx b/frontend/components/swap-transfer-tracker/transfer-row.tsx index 6927e12..1683ace 100644 --- a/frontend/components/swap-transfer-tracker/transfer-row.tsx +++ b/frontend/components/swap-transfer-tracker/transfer-row.tsx @@ -13,7 +13,7 @@ interface TransferRowProps { gridClass: string } -export default function TransferRow({ transfer, gridClass }: TransferRowProps) { +export function TransferRow({ transfer, gridClass }: TransferRowProps) { const tokenSymbol = transfer.type === 'native' ? 'MON' : 'WMON' return ( diff --git a/frontend/components/swap-transfer-tracker/transfers.tsx b/frontend/components/swap-transfer-tracker/transfers.tsx index 5b1e7e1..1b3fe6f 100644 --- a/frontend/components/swap-transfer-tracker/transfers.tsx +++ b/frontend/components/swap-transfer-tracker/transfers.tsx @@ -7,7 +7,7 @@ import { useVirtualizedList } from '@/hooks/use-virtualized-list' import { cn } from '@/lib/utils' import type { TransferData } from '@/types/transfer' import { CumulativeTransferCounter } from './cumulative-transfer-counter' -import TransferRow from './transfer-row' +import { TransferRow } from './transfer-row' interface TransfersProps { transfers: TransferData[] diff --git a/frontend/hooks/use-transfer-events.ts b/frontend/hooks/use-transfer-events.ts index d74b310..bced138 100644 --- a/frontend/hooks/use-transfer-events.ts +++ b/frontend/hooks/use-transfer-events.ts @@ -12,8 +12,7 @@ import { parseTopicsString } from '@/lib/abi-decode' import type { SerializableEventData } from '@/types/events' import type { TransferData } from '@/types/transfer' -// Uses -1 to indicate no limit on number of transfers stored -const MAX_TRANSFERS = -1 +const MAX_TRANSFERS = Infinity /** * Parse native transfer event (TxnCallFrame with non-zero value) @@ -133,12 +132,7 @@ export function useTransferEvents() { if (!transferData) return - setAllTransfers((prev) => { - if (MAX_TRANSFERS === -1) { - return [transferData, ...prev] - } - return [transferData, ...prev].slice(0, MAX_TRANSFERS) - }) + setAllTransfers((prev) => [transferData, ...prev].slice(0, MAX_TRANSFERS)) // Update cumulative total setCumulativeTransferred((prev) => prev + BigInt(transferData.value)) diff --git a/frontend/hooks/use-virtualized-list.ts b/frontend/hooks/use-virtualized-list.ts index 5b2c575..f82ca7d 100644 --- a/frontend/hooks/use-virtualized-list.ts +++ b/frontend/hooks/use-virtualized-list.ts @@ -27,20 +27,14 @@ export function useVirtualizedList({ const listRef = useRef(null) const [containerHeight, setContainerHeight] = useState(384) - // Freeze data when paused - this prevents ALL re-renders while paused + // Freeze data when paused - this prevents re-renders while paused const [displayedData, setDisplayedData] = useState(data) - const wasFollowingRef = useRef(isFollowing) - // Update displayed data only when following, or when resuming from pause + // Update displayed data only when following useEffect(() => { if (isFollowing) { setDisplayedData(data) } - // If we just resumed from pause, update to latest - if (isFollowing && !wasFollowingRef.current) { - setDisplayedData(data) - } - wasFollowingRef.current = isFollowing }, [data, isFollowing]) // Store displayed data in ref for stable rowProps