Commit 88b7acf3 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

feat: use cache while debouncing quotes (#7188)

* feat: check cache before debouncing quote

* feat: use cached values if available

* fix: initial loading state

* fix: no transition to loading

* chore: return skipToken from args

* test: update snapshots

* fix: add back stale state
parent 877e000d
...@@ -35,7 +35,8 @@ export const LoadingRows = styled.div` ...@@ -35,7 +35,8 @@ export const LoadingRows = styled.div`
export const loadingOpacityMixin = css<{ $loading: boolean }>` export const loadingOpacityMixin = css<{ $loading: boolean }>`
filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')}; filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')};
opacity: ${({ $loading }) => ($loading ? '0.4' : '1')}; opacity: ${({ $loading }) => ($loading ? '0.4' : '1')};
transition: opacity 0.2s ease-in-out; transition: ${({ $loading, theme }) =>
$loading ? 'none' : `opacity ${theme.transition.duration.medium} ${theme.transition.timing.inOut}`};
` `
export const LoadingOpacityContainer = styled.div<{ $loading: boolean }>` export const LoadingOpacityContainer = styled.div<{ $loading: boolean }>`
......
...@@ -110,8 +110,8 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -110,8 +110,8 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
-webkit-filter: none; -webkit-filter: none;
filter: none; filter: none;
opacity: 1; opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out; -webkit-transition: opacity 250ms ease-in-out;
transition: opacity 0.2s ease-in-out; transition: opacity 250ms ease-in-out;
} }
.c10 { .c10 {
......
...@@ -70,39 +70,27 @@ export function useDebouncedTrade( ...@@ -70,39 +70,27 @@ export function useDebouncedTrade(
[amountSpecified, otherCurrency] [amountSpecified, otherCurrency]
) )
const debouncedSwapQuoteFlagEnabled = useDebounceSwapQuoteFlag() === DebounceSwapQuoteVariant.Enabled const debouncedSwapQuoteFlagEnabled = useDebounceSwapQuoteFlag() === DebounceSwapQuoteVariant.Enabled
const debouncedInputs = useDebounce(inputs, debouncedSwapQuoteFlagEnabled ? DEBOUNCE_TIME_INCREASED : DEBOUNCE_TIME) const isDebouncing =
const isDebouncing = debouncedInputs !== inputs useDebounce(inputs, debouncedSwapQuoteFlagEnabled ? DEBOUNCE_TIME_INCREASED : DEBOUNCE_TIME) !== inputs
const [debouncedAmount, debouncedOtherCurrency] = debouncedInputs
const isWrap = useMemo(() => { const isWrap = useMemo(() => {
if (!chainId || !amountSpecified || !debouncedOtherCurrency) return false if (!chainId || !amountSpecified || !otherCurrency) return false
const weth = WRAPPED_NATIVE_CURRENCY[chainId] const weth = WRAPPED_NATIVE_CURRENCY[chainId]
return ( return (
(amountSpecified.currency.isNative && weth?.equals(debouncedOtherCurrency)) || (amountSpecified.currency.isNative && weth?.equals(otherCurrency)) ||
(debouncedOtherCurrency.isNative && weth?.equals(amountSpecified.currency)) (otherCurrency.isNative && weth?.equals(amountSpecified.currency))
) )
}, [amountSpecified, chainId, debouncedOtherCurrency]) }, [amountSpecified, chainId, otherCurrency])
const shouldGetTrade = !isWrap && isWindowVisible const skipFetch = isDebouncing || !autoRouterSupported || !isWindowVisible || isWrap
const [routerPreference] = useRouterPreference() const [routerPreference] = useRouterPreference()
const routingAPITrade = useRoutingAPITrade( return useRoutingAPITrade(
tradeType, tradeType,
amountSpecified ? debouncedAmount : undefined, amountSpecified,
debouncedOtherCurrency, otherCurrency,
routerPreferenceOverride ?? routerPreference, routerPreferenceOverride ?? routerPreference,
!(autoRouterSupported && shouldGetTrade), // skip fetching skipFetch,
account account
) )
// If the user is debouncing, we want to show the loading state until the debounce is complete.
const isLoading = (routingAPITrade.state === TradeState.LOADING || isDebouncing) && !isWrap
return useMemo(
() => ({
...routingAPITrade,
...(isLoading ? { state: TradeState.LOADING } : {}),
}),
[isLoading, routingAPITrade]
)
} }
import { SkipToken, skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useForceUniswapXOn } from 'featureFlags/flags/forceUniswapXOn' import { useForceUniswapXOn } from 'featureFlags/flags/forceUniswapXOn'
import { useUniswapXEnabled } from 'featureFlags/flags/uniswapx' import { useUniswapXEnabled } from 'featureFlags/flags/uniswapx'
...@@ -27,7 +28,7 @@ export function useRoutingAPIArguments({ ...@@ -27,7 +28,7 @@ export function useRoutingAPIArguments({
amount?: CurrencyAmount<Currency> amount?: CurrencyAmount<Currency>
tradeType: TradeType tradeType: TradeType
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
}): GetQuoteArgs | undefined { }): GetQuoteArgs | SkipToken {
const uniswapXEnabled = useUniswapXEnabled() const uniswapXEnabled = useUniswapXEnabled()
const uniswapXForceSyntheticQuotes = useUniswapXSyntheticQuoteEnabled() const uniswapXForceSyntheticQuotes = useUniswapXSyntheticQuoteEnabled()
const forceUniswapXOn = useForceUniswapXOn() const forceUniswapXOn = useForceUniswapXOn()
...@@ -37,7 +38,7 @@ export function useRoutingAPIArguments({ ...@@ -37,7 +38,7 @@ export function useRoutingAPIArguments({
return useMemo( return useMemo(
() => () =>
!tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut) || tokenIn.wrapped.equals(tokenOut.wrapped) !tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut) || tokenIn.wrapped.equals(tokenOut.wrapped)
? undefined ? skipToken
: { : {
account, account,
amount: amount.quotient.toString(), amount: amount.quotient.toString(),
......
...@@ -753,8 +753,8 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -753,8 +753,8 @@ exports[`disable nft on landing page does not render nft information and card 1`
-webkit-filter: none; -webkit-filter: none;
filter: none; filter: none;
opacity: 1; opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out; -webkit-transition: opacity 250ms ease-in-out;
transition: opacity 0.2s ease-in-out; transition: opacity 250ms ease-in-out;
} }
.c31 { .c31 {
...@@ -1063,8 +1063,8 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -1063,8 +1063,8 @@ exports[`disable nft on landing page does not render nft information and card 1`
-webkit-filter: none; -webkit-filter: none;
filter: none; filter: none;
opacity: 1; opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out; -webkit-transition: opacity 250ms ease-in-out;
transition: opacity 0.2s ease-in-out; transition: opacity 250ms ease-in-out;
text-align: left; text-align: left;
font-size: 36px; font-size: 36px;
line-height: 44px; line-height: 44px;
...@@ -3315,8 +3315,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -3315,8 +3315,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
-webkit-filter: none; -webkit-filter: none;
filter: none; filter: none;
opacity: 1; opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out; -webkit-transition: opacity 250ms ease-in-out;
transition: opacity 0.2s ease-in-out; transition: opacity 250ms ease-in-out;
} }
.c31 { .c31 {
...@@ -3625,8 +3625,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -3625,8 +3625,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
-webkit-filter: none; -webkit-filter: none;
filter: none; filter: none;
opacity: 1; opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out; -webkit-transition: opacity 250ms ease-in-out;
transition: opacity 0.2s ease-in-out; transition: opacity 250ms ease-in-out;
text-align: left; text-align: left;
font-size: 36px; font-size: 36px;
line-height: 44px; line-height: 44px;
......
...@@ -211,3 +211,4 @@ export const routingApi = createApi({ ...@@ -211,3 +211,4 @@ export const routingApi = createApi({
}) })
export const { useGetQuoteQuery } = routingApi export const { useGetQuoteQuery } = routingApi
export const useGetQuoteQueryState = routingApi.endpoints.getQuote.useQueryState
...@@ -7,7 +7,7 @@ import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments ...@@ -7,7 +7,7 @@ import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments
import ms from 'ms' import ms from 'ms'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useGetQuoteQuery } from './slice' import { useGetQuoteQuery, useGetQuoteQueryState } from './slice'
import { import {
ClassicTrade, ClassicTrade,
InterfaceTrade, InterfaceTrade,
...@@ -78,55 +78,52 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -78,55 +78,52 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
account, account,
tokenIn: currencyIn, tokenIn: currencyIn,
tokenOut: currencyOut, tokenOut: currencyOut,
amount: skipFetch ? undefined : amountSpecified, amount: amountSpecified,
tradeType, tradeType,
routerPreference, routerPreference,
}) })
const { const { isError, data: tradeResult, error, currentData } = useGetQuoteQueryState(queryArgs)
isError, useGetQuoteQuery(skipFetch ? skipToken : queryArgs, {
data: tradeResult,
error,
currentData: currentTradeResult,
} = useGetQuoteQuery(queryArgs ?? skipToken, {
// Price-fetching is informational and costly, so it's done less frequently. // Price-fetching is informational and costly, so it's done less frequently.
pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms(`1m`) : AVERAGE_L1_BLOCK_TIME, pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms(`1m`) : AVERAGE_L1_BLOCK_TIME,
// If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period // If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period
refetchOnMountOrArgChange: 2 * 60, refetchOnMountOrArgChange: 2 * 60,
}) })
const isCurrent = currentTradeResult === tradeResult const isFetching = currentData !== tradeResult || !currentData
return useMemo(() => { return useMemo(() => {
if (skipFetch && amountSpecified) { if (amountSpecified && queryArgs === skipToken) {
// If we don't want to fetch new trades, but have valid inputs, return the stale trade. return {
return { state: TradeState.STALE, trade: tradeResult?.trade, swapQuoteLatency: tradeResult?.latencyMs } state: TradeState.STALE,
} else if (!amountSpecified || isError || !queryArgs) { trade: tradeResult?.trade,
swapQuoteLatency: tradeResult?.latencyMs,
}
} else if (!amountSpecified || isError || queryArgs === skipToken) {
return { return {
state: TradeState.INVALID, state: TradeState.INVALID,
trade: undefined, trade: undefined,
error: JSON.stringify(error), error: JSON.stringify(error),
} }
} else if (tradeResult?.state === QuoteState.NOT_FOUND && isCurrent) { } else if (tradeResult?.state === QuoteState.NOT_FOUND && !isFetching) {
return TRADE_NOT_FOUND return TRADE_NOT_FOUND
} else if (!tradeResult?.trade) { } else if (!tradeResult?.trade) {
// TODO(WEB-1985): use `isLoading` returned by rtk-query hook instead of checking for `trade` status
return TRADE_LOADING return TRADE_LOADING
} else { } else {
return { return {
state: isCurrent ? TradeState.VALID : TradeState.LOADING, state: isFetching ? TradeState.LOADING : TradeState.VALID,
trade: tradeResult.trade, trade: tradeResult?.trade,
swapQuoteLatency: tradeResult.latencyMs, swapQuoteLatency: tradeResult?.latencyMs,
} }
} }
}, [ }, [
amountSpecified, amountSpecified,
error, error,
isCurrent,
isError, isError,
isFetching,
queryArgs, queryArgs,
skipFetch,
tradeResult?.state,
tradeResult?.latencyMs, tradeResult?.latencyMs,
tradeResult?.state,
tradeResult?.trade, tradeResult?.trade,
]) ])
} }
......
...@@ -5,7 +5,7 @@ import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance' ...@@ -5,7 +5,7 @@ import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useDebouncedTrade } from 'hooks/useDebouncedTrade' import { useDebouncedTrade } from 'hooks/useDebouncedTrade'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ParsedQs } from 'qs' import { ParsedQs } from 'qs'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { ReactNode, useCallback, useEffect, useMemo } from 'react'
import { AnyAction } from 'redux' import { AnyAction } from 'redux'
import { useAppDispatch } from 'state/hooks' import { useAppDispatch } from 'state/hooks'
import { InterfaceTrade, TradeState } from 'state/routing/types' import { InterfaceTrade, TradeState } from 'state/routing/types'
...@@ -90,7 +90,6 @@ export type SwapInfo = { ...@@ -90,7 +90,6 @@ export type SwapInfo = {
// from the current swap inputs, compute the best trade and return it. // from the current swap inputs, compute the best trade and return it.
export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefined): SwapInfo { export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefined): SwapInfo {
const { account } = useWeb3React() const { account } = useWeb3React()
const [previouslyInvalid, setPreviouslyInvalid] = useState(false)
const { const {
independentField, independentField,
...@@ -116,7 +115,7 @@ export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefine ...@@ -116,7 +115,7 @@ export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefine
[inputCurrency, isExactIn, outputCurrency, typedValue] [inputCurrency, isExactIn, outputCurrency, typedValue]
) )
let trade = useDebouncedTrade( const trade = useDebouncedTrade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
parsedAmount, parsedAmount,
(isExactIn ? outputCurrency : inputCurrency) ?? undefined, (isExactIn ? outputCurrency : inputCurrency) ?? undefined,
...@@ -124,25 +123,6 @@ export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefine ...@@ -124,25 +123,6 @@ export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefine
account account
) )
const nextPreviouslyInvalid = (() => {
if (trade.state === TradeState.INVALID) {
return true
} else if (trade.state !== TradeState.LOADING) {
return false
}
return undefined
})()
if (typeof nextPreviouslyInvalid === 'boolean' && nextPreviouslyInvalid !== previouslyInvalid) {
setPreviouslyInvalid(nextPreviouslyInvalid)
}
if (trade.state == TradeState.LOADING && previouslyInvalid) {
trade = {
...trade,
trade: undefined,
}
}
const currencyBalances = useMemo( const currencyBalances = useMemo(
() => ({ () => ({
[Field.INPUT]: relevantTokenBalances[0], [Field.INPUT]: relevantTokenBalances[0],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment