diff --git a/src/components/SwapPanel.tsx b/src/components/SwapPanel.tsx index 4554d6d..793c495 100644 --- a/src/components/SwapPanel.tsx +++ b/src/components/SwapPanel.tsx @@ -4,7 +4,7 @@ 'use client'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useAccount, useChainId } from 'wagmi'; import { useSwapState, useSwapExecution, getSwapStageMessage, SwapStage } from '@/hooks'; import { getTokensForChain } from '@/config/tokens'; @@ -32,6 +32,14 @@ export function SwapPanel() { const swap = useSwapState(); const execution = useSwapExecution(); const receiptModal = useReceiptModal(); + const quoteRefresh = useRef(swap.refreshQuote); + const quoteTimer = useRef | undefined>(undefined); + + useEffect(() => { + quoteRefresh.current = swap.refreshQuote; + }, [swap.refreshQuote]); + + useEffect(() => () => clearTimeout(quoteTimer.current), []); const [showInputTokens, setShowInputTokens] = useState(false); const [showOutputTokens, setShowOutputTokens] = useState(false); @@ -70,30 +78,25 @@ export function SwapPanel() { } }; + const scheduleQuoteRefresh = (amount: string, hasTokens: boolean) => { + clearTimeout(quoteTimer.current); + if (amount && hasTokens) { + quoteTimer.current = setTimeout(() => { + void quoteRefresh.current(); + }, 500); + } + }; + const handleAmountChange = (e: React.ChangeEvent) => { const value = e.target.value; swap.setInputAmount(value); - if (value && swap.outputToken) { - const timer = setTimeout(() => { - if (value === e.target.value) { - swap.refreshQuote(); - } - }, 500); - return () => clearTimeout(timer); - } + scheduleQuoteRefresh(value, !!swap.outputToken); }; const handleOutputAmountChange = (e: React.ChangeEvent) => { const value = e.target.value; swap.setOutputAmount(value); - if (value && swap.inputToken) { - const timer = setTimeout(() => { - if (value === e.target.value) { - swap.refreshQuote(); - } - }, 500); - return () => clearTimeout(timer); - } + scheduleQuoteRefresh(value, !!swap.inputToken); }; const handleSlippageChange = (e: React.ChangeEvent) => { diff --git a/src/hooks/useSwapState.ts b/src/hooks/useSwapState.ts index e2db83a..5c0d926 100644 --- a/src/hooks/useSwapState.ts +++ b/src/hooks/useSwapState.ts @@ -4,7 +4,7 @@ 'use client'; -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import { useChainId } from 'wagmi'; import type { SupportedChainId } from '@/config/chains'; import { getToken, getTokenAddress, type Token } from '@/config/tokens'; @@ -83,6 +83,7 @@ function getDefaultSwapTokens(chainId: SupportedChainId): { export function useSwapState(): SwapStateType { const chainId = useChainId() as SupportedChainId; const defaultTokens = getDefaultSwapTokens(chainId); + const quoteRequestId = useRef(0); const [inputToken, setInputToken] = useState(defaultTokens.inputToken); const [outputToken, setOutputToken] = useState(defaultTokens.outputToken); @@ -101,6 +102,8 @@ export function useSwapState(): SwapStateType { const quoteExpired = quote && (Date.now() - quoteTimestamp > QUOTE_EXPIRY_MS); useEffect(() => { + quoteRequestId.current += 1; + setIsLoadingQuote(false); const nextDefaults = getDefaultSwapTokens(chainId); setInputToken(nextDefaults.inputToken); setOutputToken(nextDefaults.outputToken); @@ -115,6 +118,8 @@ export function useSwapState(): SwapStateType { }, [chainId]); const clearQuoteState = useCallback(() => { + quoteRequestId.current += 1; + setIsLoadingQuote(false); setQuoteData(null); setAvailableRoutes([]); setSelectedRouteId(null); @@ -169,7 +174,9 @@ export function useSwapState(): SwapStateType { return; } + const requestId = quoteRequestId.current; const { selectRouteQuote } = await import('@/lib/swapRouter'); + if (requestId !== quoteRequestId.current) return; const nextQuote = selectRouteQuote(quote, routeId); setQuoteData(nextQuote); setAvailableRoutes(nextQuote.routes ?? []); @@ -178,9 +185,11 @@ export function useSwapState(): SwapStateType { }, [quote]); const handleRefreshQuote = useCallback(async () => { + const requestId = ++quoteRequestId.current; const quoteAmount = tradeType === 'exactOut' ? outputAmount : inputAmount; if (!inputToken || !outputToken || !quoteAmount) { + setIsLoadingQuote(false); setQuoteError('Missing token or amount for quote refresh'); return; } @@ -202,15 +211,17 @@ export function useSwapState(): SwapStateType { preferredRouteId: selectedRouteId ?? undefined, tradeType, }); + if (requestId !== quoteRequestId.current) return; handleSetQuote(newQuote); } catch (error) { const { handleRoutingError } = await import('@/lib/swapErrors'); + if (requestId !== quoteRequestId.current) return; const swapError = handleRoutingError(error); const message = swapError.message; setQuoteError(message); setLastError(message); } finally { - setIsLoadingQuote(false); + if (requestId === quoteRequestId.current) setIsLoadingQuote(false); } }, [chainId, handleSetQuote, inputAmount, inputToken, outputAmount, outputToken, selectedRouteId, tradeType]); @@ -219,6 +230,8 @@ export function useSwapState(): SwapStateType { }, []); const handleReset = useCallback(() => { + quoteRequestId.current += 1; + setIsLoadingQuote(false); const nextDefaults = getDefaultSwapTokens(chainId); setInputToken(nextDefaults.inputToken); setOutputToken(nextDefaults.outputToken);