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
This commit is contained in:
Zach Pomerantz 2023-08-18 12:36:24 -07:00 committed by GitHub
parent 877e000da6
commit 88b7acf3ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 47 additions and 79 deletions

@ -35,7 +35,8 @@ export const LoadingRows = styled.div`
export const loadingOpacityMixin = css<{ $loading: boolean }>`
filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')};
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 }>`

@ -110,8 +110,8 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
-webkit-filter: none;
filter: none;
opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out;
transition: opacity 0.2s ease-in-out;
-webkit-transition: opacity 250ms ease-in-out;
transition: opacity 250ms ease-in-out;
}
.c10 {

@ -70,39 +70,27 @@ export function useDebouncedTrade(
[amountSpecified, otherCurrency]
)
const debouncedSwapQuoteFlagEnabled = useDebounceSwapQuoteFlag() === DebounceSwapQuoteVariant.Enabled
const debouncedInputs = useDebounce(inputs, debouncedSwapQuoteFlagEnabled ? DEBOUNCE_TIME_INCREASED : DEBOUNCE_TIME)
const isDebouncing = debouncedInputs !== inputs
const [debouncedAmount, debouncedOtherCurrency] = debouncedInputs
const isDebouncing =
useDebounce(inputs, debouncedSwapQuoteFlagEnabled ? DEBOUNCE_TIME_INCREASED : DEBOUNCE_TIME) !== inputs
const isWrap = useMemo(() => {
if (!chainId || !amountSpecified || !debouncedOtherCurrency) return false
if (!chainId || !amountSpecified || !otherCurrency) return false
const weth = WRAPPED_NATIVE_CURRENCY[chainId]
return (
(amountSpecified.currency.isNative && weth?.equals(debouncedOtherCurrency)) ||
(debouncedOtherCurrency.isNative && weth?.equals(amountSpecified.currency))
(amountSpecified.currency.isNative && weth?.equals(otherCurrency)) ||
(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 routingAPITrade = useRoutingAPITrade(
return useRoutingAPITrade(
tradeType,
amountSpecified ? debouncedAmount : undefined,
debouncedOtherCurrency,
amountSpecified,
otherCurrency,
routerPreferenceOverride ?? routerPreference,
!(autoRouterSupported && shouldGetTrade), // skip fetching
skipFetch,
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]
)
}

@ -1,3 +1,4 @@
import { SkipToken, skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useForceUniswapXOn } from 'featureFlags/flags/forceUniswapXOn'
import { useUniswapXEnabled } from 'featureFlags/flags/uniswapx'
@ -27,7 +28,7 @@ export function useRoutingAPIArguments({
amount?: CurrencyAmount<Currency>
tradeType: TradeType
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
}): GetQuoteArgs | undefined {
}): GetQuoteArgs | SkipToken {
const uniswapXEnabled = useUniswapXEnabled()
const uniswapXForceSyntheticQuotes = useUniswapXSyntheticQuoteEnabled()
const forceUniswapXOn = useForceUniswapXOn()
@ -37,7 +38,7 @@ export function useRoutingAPIArguments({
return useMemo(
() =>
!tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut) || tokenIn.wrapped.equals(tokenOut.wrapped)
? undefined
? skipToken
: {
account,
amount: amount.quotient.toString(),

@ -753,8 +753,8 @@ exports[`disable nft on landing page does not render nft information and card 1`
-webkit-filter: none;
filter: none;
opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out;
transition: opacity 0.2s ease-in-out;
-webkit-transition: opacity 250ms ease-in-out;
transition: opacity 250ms ease-in-out;
}
.c31 {
@ -1063,8 +1063,8 @@ exports[`disable nft on landing page does not render nft information and card 1`
-webkit-filter: none;
filter: none;
opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out;
transition: opacity 0.2s ease-in-out;
-webkit-transition: opacity 250ms ease-in-out;
transition: opacity 250ms ease-in-out;
text-align: left;
font-size: 36px;
line-height: 44px;
@ -3315,8 +3315,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
-webkit-filter: none;
filter: none;
opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out;
transition: opacity 0.2s ease-in-out;
-webkit-transition: opacity 250ms ease-in-out;
transition: opacity 250ms ease-in-out;
}
.c31 {
@ -3625,8 +3625,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
-webkit-filter: none;
filter: none;
opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out;
transition: opacity 0.2s ease-in-out;
-webkit-transition: opacity 250ms ease-in-out;
transition: opacity 250ms ease-in-out;
text-align: left;
font-size: 36px;
line-height: 44px;

@ -211,3 +211,4 @@ export const routingApi = createApi({
})
export const { useGetQuoteQuery } = routingApi
export const useGetQuoteQueryState = routingApi.endpoints.getQuote.useQueryState

@ -7,7 +7,7 @@ import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments
import ms from 'ms'
import { useMemo } from 'react'
import { useGetQuoteQuery } from './slice'
import { useGetQuoteQuery, useGetQuoteQueryState } from './slice'
import {
ClassicTrade,
InterfaceTrade,
@ -78,55 +78,52 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
account,
tokenIn: currencyIn,
tokenOut: currencyOut,
amount: skipFetch ? undefined : amountSpecified,
amount: amountSpecified,
tradeType,
routerPreference,
})
const {
isError,
data: tradeResult,
error,
currentData: currentTradeResult,
} = useGetQuoteQuery(queryArgs ?? skipToken, {
const { isError, data: tradeResult, error, currentData } = useGetQuoteQueryState(queryArgs)
useGetQuoteQuery(skipFetch ? skipToken : queryArgs, {
// Price-fetching is informational and costly, so it's done less frequently.
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
refetchOnMountOrArgChange: 2 * 60,
})
const isCurrent = currentTradeResult === tradeResult
const isFetching = currentData !== tradeResult || !currentData
return useMemo(() => {
if (skipFetch && amountSpecified) {
// If we don't want to fetch new trades, but have valid inputs, return the stale trade.
return { state: TradeState.STALE, trade: tradeResult?.trade, swapQuoteLatency: tradeResult?.latencyMs }
} else if (!amountSpecified || isError || !queryArgs) {
if (amountSpecified && queryArgs === skipToken) {
return {
state: TradeState.STALE,
trade: tradeResult?.trade,
swapQuoteLatency: tradeResult?.latencyMs,
}
} else if (!amountSpecified || isError || queryArgs === skipToken) {
return {
state: TradeState.INVALID,
trade: undefined,
error: JSON.stringify(error),
}
} else if (tradeResult?.state === QuoteState.NOT_FOUND && isCurrent) {
} else if (tradeResult?.state === QuoteState.NOT_FOUND && !isFetching) {
return TRADE_NOT_FOUND
} else if (!tradeResult?.trade) {
// TODO(WEB-1985): use `isLoading` returned by rtk-query hook instead of checking for `trade` status
return TRADE_LOADING
} else {
return {
state: isCurrent ? TradeState.VALID : TradeState.LOADING,
trade: tradeResult.trade,
swapQuoteLatency: tradeResult.latencyMs,
state: isFetching ? TradeState.LOADING : TradeState.VALID,
trade: tradeResult?.trade,
swapQuoteLatency: tradeResult?.latencyMs,
}
}
}, [
amountSpecified,
error,
isCurrent,
isError,
isFetching,
queryArgs,
skipFetch,
tradeResult?.state,
tradeResult?.latencyMs,
tradeResult?.state,
tradeResult?.trade,
])
}

@ -5,7 +5,7 @@ import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useDebouncedTrade } from 'hooks/useDebouncedTrade'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ParsedQs } from 'qs'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { ReactNode, useCallback, useEffect, useMemo } from 'react'
import { AnyAction } from 'redux'
import { useAppDispatch } from 'state/hooks'
import { InterfaceTrade, TradeState } from 'state/routing/types'
@ -90,7 +90,6 @@ export type SwapInfo = {
// from the current swap inputs, compute the best trade and return it.
export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefined): SwapInfo {
const { account } = useWeb3React()
const [previouslyInvalid, setPreviouslyInvalid] = useState(false)
const {
independentField,
@ -116,7 +115,7 @@ export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefine
[inputCurrency, isExactIn, outputCurrency, typedValue]
)
let trade = useDebouncedTrade(
const trade = useDebouncedTrade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
parsedAmount,
(isExactIn ? outputCurrency : inputCurrency) ?? undefined,
@ -124,25 +123,6 @@ export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefine
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(
() => ({
[Field.INPUT]: relevantTokenBalances[0],