From 56aecb394b90b69d75de21c7b2cde533c6136ce5 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 29 May 2024 15:33:45 -0300 Subject: [PATCH] Gas optimizations (#5779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf * ✨ * useWhyDidYouUpdate * EstimatedSwapGasFee * keepPreviousData * AnimatedText * isSameAddress --- .../Swap/components/EstimatedSwapGasFee.tsx | 36 +++++++++++ .../screens/Swap/components/GasButton.tsx | 35 ++++++----- .../screens/Swap/components/GasPanel.tsx | 41 ++++++++----- .../screens/Swap/components/ReviewPanel.tsx | 9 +-- .../screens/Swap/hooks/useEstimatedGasFee.ts | 56 +++++++++++++---- .../screens/Swap/hooks/useSelectedGas.ts | 17 +++--- .../Swap/hooks/useSwapEstimatedGasLimit.ts | 11 +++- src/__swaps__/utils/meteorology.ts | 60 ++++++++++++++----- src/hooks/useWhyDidYouUpdate.ts | 47 +++++++++++++++ 9 files changed, 235 insertions(+), 77 deletions(-) create mode 100644 src/__swaps__/screens/Swap/components/EstimatedSwapGasFee.tsx create mode 100644 src/hooks/useWhyDidYouUpdate.ts diff --git a/src/__swaps__/screens/Swap/components/EstimatedSwapGasFee.tsx b/src/__swaps__/screens/Swap/components/EstimatedSwapGasFee.tsx new file mode 100644 index 00000000000..9d281c3f425 --- /dev/null +++ b/src/__swaps__/screens/Swap/components/EstimatedSwapGasFee.tsx @@ -0,0 +1,36 @@ +import { AnimatedText, TextProps } from '@/design-system'; +import React, { memo } from 'react'; +import { useAnimatedStyle, withRepeat, withSequence, withSpring, withTiming } from 'react-native-reanimated'; + +import { pulsingConfig, sliderConfig } from '../constants'; +import { GasSettings } from '../hooks/useCustomGas'; +import { useSwapEstimatedGasFee } from '../hooks/useEstimatedGasFee'; + +export const EstimatedSwapGasFee = memo(function EstimatedGasFeeA({ + gasSettings, + align, + color = 'labelTertiary', + size = '15pt', + weight = 'bold', + tabularNumbers = true, +}: { gasSettings: GasSettings | undefined } & Partial>) { + const { data: estimatedGasFee = '--', isLoading } = useSwapEstimatedGasFee(gasSettings); + + const animatedOpacity = useAnimatedStyle(() => ({ + opacity: isLoading + ? withRepeat(withSequence(withTiming(0.5, pulsingConfig), withTiming(1, pulsingConfig)), -1, true) + : withSpring(1, sliderConfig), + })); + + return ( + + ); +}); diff --git a/src/__swaps__/screens/Swap/components/GasButton.tsx b/src/__swaps__/screens/Swap/components/GasButton.tsx index ae9fa1620ae..1841c8e4005 100644 --- a/src/__swaps__/screens/Swap/components/GasButton.tsx +++ b/src/__swaps__/screens/Swap/components/GasButton.tsx @@ -1,42 +1,39 @@ import { ChainId } from '@/__swaps__/types/chains'; import { weiToGwei } from '@/__swaps__/utils/ethereum'; -import { useMeteorologySuggestions } from '@/__swaps__/utils/meteorology'; +import { getCachedCurrentBaseFee, useMeteorologySuggestions } from '@/__swaps__/utils/meteorology'; import { add } from '@/__swaps__/utils/numbers'; import { ButtonPressAnimation } from '@/components/animations'; import { ContextMenu } from '@/components/context-menu'; import { Centered } from '@/components/layout'; import ContextMenuButton from '@/components/native-context-menu/contextMenu'; -import { Box, Inline, Stack, Text, TextIcon, useColorMode, useForegroundColor } from '@/design-system'; +import { Box, Inline, Text, TextIcon, useColorMode, useForegroundColor } from '@/design-system'; import { IS_ANDROID } from '@/env'; import * as i18n from '@/languages'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import styled from '@/styled-thing'; import { gasUtils } from '@/utils'; import React, { ReactNode, useCallback, useMemo } from 'react'; +import { StyleSheet } from 'react-native'; import { runOnUI } from 'react-native-reanimated'; import { ETH_COLOR, ETH_COLOR_DARK, THICK_BORDER_WIDTH } from '../constants'; import { formatNumber } from '../hooks/formatNumber'; import { GasSettings, useCustomGasSettings } from '../hooks/useCustomGas'; -import { useSwapEstimatedGasFee } from '../hooks/useEstimatedGasFee'; import { GasSpeed, setSelectedGasSpeed, useSelectedGas, useSelectedGasSpeed } from '../hooks/useSelectedGas'; import { useSwapContext } from '../providers/swap-provider'; -import { StyleSheet } from 'react-native'; +import { EstimatedSwapGasFee } from './EstimatedSwapGasFee'; const { GAS_ICONS } = gasUtils; function EstimatedGasFee() { const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet); const gasSettings = useSelectedGas(chainId); - const estimatedGasFee = useSwapEstimatedGasFee(gasSettings); return ( 􀵟 - - {estimatedGasFee} - + ); } @@ -66,17 +63,19 @@ const GasSpeedPagerCentered = styled(Centered).attrs(() => ({ marginHorizontal: 8, }))({}); -function getEstimatedFeeRangeInGwei(gasSettings: GasSettings | undefined, currentBaseFee?: string | undefined) { +function getEstimatedFeeRangeInGwei(gasSettings: GasSettings | undefined, currentBaseFee: string | undefined) { if (!gasSettings) return undefined; if (!gasSettings.isEIP1559) return `${formatNumber(weiToGwei(gasSettings.gasPrice))} Gwei`; const { maxBaseFee, maxPriorityFee } = gasSettings; - return `${formatNumber(weiToGwei(add(maxBaseFee, maxPriorityFee)))} Gwei`; + const maxFee = formatNumber(weiToGwei(add(maxBaseFee, maxPriorityFee))); + + if (!currentBaseFee) return `${maxFee} Gwei`; + + const minFee = formatNumber(weiToGwei(add(currentBaseFee, maxPriorityFee))); - // return `${formatNumber(weiToGwei(add(baseFee, maxPriorityFee)))} - ${formatNumber( - // weiToGwei(add(maxBaseFee, maxPriorityFee)) - // )} Gwei`; + return `${minFee} - ${maxFee} Gwei`; } function keys(obj: Record | undefined) { @@ -118,9 +117,9 @@ const GasMenu = ({ children }: { children: ReactNode }) => { const menuItems = menuOptions.map(gasOption => { if (IS_ANDROID) return gasOption; - // const currentBaseFee = getCachedCurrentBaseFee(chainId); + const currentBaseFee = getCachedCurrentBaseFee(chainId); const gasSettings = gasOption === 'custom' ? customGasSettings : metereologySuggestions.data?.[gasOption]; - const subtitle = getEstimatedFeeRangeInGwei(gasSettings); + const subtitle = getEstimatedFeeRangeInGwei(gasSettings, currentBaseFee); return { actionKey: gasOption, @@ -130,7 +129,7 @@ const GasMenu = ({ children }: { children: ReactNode }) => { }; }); return { menuItems, menuTitle: '' }; - }, [customGasSettings, menuOptions, metereologySuggestions.data]); + }, [customGasSettings, menuOptions, metereologySuggestions.data, chainId]); if (metereologySuggestions.isLoading) return children; @@ -211,10 +210,10 @@ export function ReviewGasButton() { export const GasButton = () => { return ( - + - + ); }; diff --git a/src/__swaps__/screens/Swap/components/GasPanel.tsx b/src/__swaps__/screens/Swap/components/GasPanel.tsx index 597804ec7b4..2402552e183 100644 --- a/src/__swaps__/screens/Swap/components/GasPanel.tsx +++ b/src/__swaps__/screens/Swap/components/GasPanel.tsx @@ -1,5 +1,5 @@ import * as i18n from '@/languages'; -import React, { PropsWithChildren } from 'react'; +import React, { PropsWithChildren, useMemo } from 'react'; import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'; import { fadeConfig } from '@/__swaps__/screens/Swap/constants'; @@ -11,9 +11,10 @@ import { useBaseFee, useGasTrend, useIsChainEIP1559, - useMeteorologySuggestions, + useMeteorologySuggestion, } from '@/__swaps__/utils/meteorology'; import { add, subtract } from '@/__swaps__/utils/numbers'; +import { opacity } from '@/__swaps__/utils/swaps'; import { ButtonPressAnimation } from '@/components/animations'; import { Box, Inline, Separator, Stack, Text, globalColors, useColorMode, useForegroundColor } from '@/design-system'; import { IS_ANDROID } from '@/env'; @@ -25,9 +26,8 @@ import { useSwapsStore } from '@/state/swaps/swapsStore'; import { upperFirst } from 'lodash'; import { formatNumber } from '../hooks/formatNumber'; import { GasSettings, getCustomGasSettings, setCustomGasSettings, useCustomGasStore } from '../hooks/useCustomGas'; -import { useSwapEstimatedGasFee } from '../hooks/useEstimatedGasFee'; import { setSelectedGasSpeed, useSelectedGasSpeed } from '../hooks/useSelectedGas'; -import { opacity } from '@/__swaps__/utils/swaps'; +import { EstimatedSwapGasFee } from './EstimatedSwapGasFee'; const MINER_TIP_TYPE = 'minerTip'; const MAX_BASE_FEE_TYPE = 'maxBaseFee'; @@ -144,6 +144,9 @@ function CurrentBaseFee() { // loading state? + const isEIP1559 = useIsChainEIP1559(chainId); + if (!isEIP1559) return null; + return ( s.inputAsset?.chainId || ChainId.mainnet); - const selectedSpeed = useSelectedGasSpeed(chainId); const currentGasSettings = useCustomGasStore(s => select(s?.[chainId])); - const { data: suggestion } = useMeteorologySuggestions({ + const speed = useSelectedGasSpeed(chainId); + const { data: suggestion } = useMeteorologySuggestion({ chainId, - select: d => select(selectedSpeed === 'custom' ? undefined : d[selectedSpeed]), - enabled: !state && selectedSpeed !== 'custom', + speed, + select, + enabled: !!state, + notifyOnChangeProps: !!state && speed !== 'custom' ? ['data'] : [], }); - return state ?? currentGasSettings ?? suggestion; + return useMemo(() => state ?? currentGasSettings ?? suggestion, [currentGasSettings, state, suggestion]); } const setGasPanelState = (update: Partial) => { @@ -258,12 +263,12 @@ const stateToGasSettings = (s: GasPanelState | undefined): GasSettings | undefin if (s.gasPrice) return { isEIP1559: false, gasPrice: s.gasPrice || '0' }; return { isEIP1559: true, maxBaseFee: s.maxBaseFee || '0', maxPriorityFee: s.maxPriorityFee || '0' }; }; + function MaxTransactionFee() { const { isDarkMode } = useColorMode(); const gasPanelState = useGasPanelState(); - const gasSettings = stateToGasSettings(gasPanelState); - const maxTransactionFee = useSwapEstimatedGasFee(gasSettings); + const gasSettings = useMemo(() => stateToGasSettings(gasPanelState), [gasPanelState]); return ( @@ -279,9 +284,13 @@ function MaxTransactionFee() { - - {maxTransactionFee} - + ); @@ -290,7 +299,9 @@ function MaxTransactionFee() { function EditableGasSettings() { const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet); const isEIP1559 = useIsChainEIP1559(chainId); + if (!isEIP1559) return ; + return ( <> @@ -300,8 +311,6 @@ function EditableGasSettings() { } function saveCustomGasSettings() { - // input is debounced if the time between editing and closing the panel is less than the debounce time (500ms) it's gonna be outdated - const unsaved = useGasPanelStore.getState(); const { inputAsset } = useSwapsStore.getState(); diff --git a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx index 392c632d1f0..511f5f3b182 100644 --- a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx +++ b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx @@ -28,8 +28,8 @@ import { chainNameForChainIdWithMainnetSubstitutionWorklet } from '@/__swaps__/u import { useEstimatedTime } from '@/__swaps__/utils/meteorology'; import { convertRawAmountToBalance, convertRawAmountToNativeDisplay, handleSignificantDecimals, multiply } from '@/__swaps__/utils/numbers'; import { useSwapsStore } from '@/state/swaps/swapsStore'; -import { useSwapEstimatedGasFee } from '../hooks/useEstimatedGasFee'; import { useSelectedGas, useSelectedGasSpeed } from '../hooks/useSelectedGas'; +import { EstimatedSwapGasFee } from './EstimatedSwapGasFee'; const unknown = i18n.t(i18n.l.swap.unknown); @@ -89,13 +89,8 @@ const RainbowFee = () => { function EstimatedGasFee() { const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet); const gasSettings = useSelectedGas(chainId); - const estimatedGasFee = useSwapEstimatedGasFee(gasSettings); - return ( - - {estimatedGasFee} - - ); + return ; } function EstimatedArrivalTime() { diff --git a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts index 6e01456917b..6e858ae7424 100644 --- a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts +++ b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts @@ -3,11 +3,21 @@ import { weiToGwei } from '@/__swaps__/utils/ethereum'; import { add, multiply } from '@/__swaps__/utils/numbers'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import ethereumUtils, { useNativeAssetForNetwork } from '@/utils/ethereumUtils'; -import { formatUnits } from 'viem'; +import { ETH_ADDRESS } from '@rainbow-me/swaps'; +import { useMemo } from 'react'; +import { formatUnits, zeroAddress } from 'viem'; import { formatCurrency, formatNumber } from './formatNumber'; import { GasSettings } from './useCustomGas'; import { useSwapEstimatedGasLimit } from './useSwapEstimatedGasLimit'; +function safeBigInt(value: string) { + try { + return BigInt(value); + } catch { + return 0n; + } +} + export function useEstimatedGasFee({ chainId, gasLimit, @@ -20,28 +30,52 @@ export function useEstimatedGasFee({ const network = ethereumUtils.getNetworkFromChainId(chainId); const nativeNetworkAsset = useNativeAssetForNetwork(network); - if (!gasLimit || !gasSettings || !nativeNetworkAsset) return 'Loading...'; // TODO: loading state + return useMemo(() => { + if (!gasLimit || !gasSettings || !nativeNetworkAsset?.price) return; - const amount = gasSettings.isEIP1559 ? add(gasSettings.maxBaseFee, gasSettings.maxPriorityFee) : gasSettings.gasPrice; + const amount = gasSettings.isEIP1559 ? add(gasSettings.maxBaseFee, gasSettings.maxPriorityFee) : gasSettings.gasPrice; - const totalWei = multiply(gasLimit, amount); - const nativePrice = nativeNetworkAsset.price.value?.toString(); + const totalWei = multiply(gasLimit, amount); + const networkAssetPrice = nativeNetworkAsset.price.value?.toString(); - if (!nativePrice) return `${formatNumber(weiToGwei(totalWei))} Gwei`; + if (!networkAssetPrice) return `${formatNumber(weiToGwei(totalWei))} Gwei`; - const gasAmount = formatUnits(BigInt(totalWei), nativeNetworkAsset.decimals).toString(); - const feeInUserCurrency = multiply(nativePrice, gasAmount); + const gasAmount = formatUnits(safeBigInt(totalWei), nativeNetworkAsset.decimals).toString(); + const feeInUserCurrency = multiply(networkAssetPrice, gasAmount); - return formatCurrency(feeInUserCurrency); + return formatCurrency(feeInUserCurrency); + }, [gasLimit, gasSettings, nativeNetworkAsset]); } +const eth = ETH_ADDRESS.toLowerCase(); +const isEth = (address: string) => [eth, zeroAddress, 'eth'].includes(address.toLowerCase()); +const isSameAddress = (a: string, b: string) => { + if (isEth(a) && isEth(b)) return true; + return a.toLowerCase() === b.toLowerCase(); +}; export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) { const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet); const assetToSell = useSwapsStore(s => s.inputAsset); + const assetToBuy = useSwapsStore(s => s.outputAsset); const quote = useSwapsStore(s => s.quote); - const { data: gasLimit } = useSwapEstimatedGasLimit({ chainId, quote, assetToSell }, { enabled: !!quote }); + const { data: gasLimit, isFetching } = useSwapEstimatedGasLimit( + { chainId, quote, assetToSell }, + { + enabled: + !!quote && + !!assetToSell && + !!assetToBuy && + !('error' in quote) && + // the quote and the input/output assets are not updated together, + // we shouldn't try to estimate if the assets are not the same as the quote (probably still fetching a quote) + isSameAddress(quote.sellTokenAddress, assetToSell.address) && + isSameAddress(quote.buyTokenAddress, assetToBuy.address), + } + ); + + const estimatedFee = useEstimatedGasFee({ chainId, gasLimit, gasSettings }); - return useEstimatedGasFee({ chainId, gasLimit, gasSettings }); + return useMemo(() => ({ isLoading: isFetching, data: estimatedFee }), [estimatedFee, isFetching]); } diff --git a/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts b/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts index 667d0b46c74..0b61b0c1047 100644 --- a/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts +++ b/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts @@ -18,19 +18,22 @@ export const useSelectedGasSpeed = (chainId: ChainId) => export const setSelectedGasSpeed = (chainId: ChainId, speed: GasSpeed) => useSelectedGasSpeedStore.setState({ [chainId]: speed }); export const getSelectedGasSpeed = (chainId: ChainId) => useSelectedGasSpeedStore.getState()[chainId] || 'fast'; -export function useSelectedGas(chainId: ChainId) { - const selectedGasSpeed = useSelectedGasSpeed(chainId); - +export function useGasSettings(chainId: ChainId, speed: GasSpeed) { const userCustomGasSettings = useCustomGasSettings(chainId); const { data: metereologySuggestions } = useMeteorologySuggestions({ chainId, - enabled: selectedGasSpeed !== 'custom', + enabled: speed !== 'custom', }); return useMemo(() => { - if (selectedGasSpeed === 'custom') return userCustomGasSettings; - return metereologySuggestions?.[selectedGasSpeed]; - }, [selectedGasSpeed, userCustomGasSettings, metereologySuggestions]); + if (speed === 'custom') return userCustomGasSettings; + return metereologySuggestions?.[speed]; + }, [speed, userCustomGasSettings, metereologySuggestions]); +} + +export function useSelectedGas(chainId: ChainId) { + const selectedGasSpeed = useSelectedGasSpeed(chainId); + return useGasSettings(chainId, selectedGasSpeed); } export function getGasSettings(speed: GasSpeed, chainId: ChainId) { diff --git a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts index 905a2ae1c58..c30913f3da9 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts @@ -25,7 +25,7 @@ export type EstimateSwapGasLimitArgs = { // Query Key const estimateSwapGasLimitQueryKey = ({ chainId, quote, assetToSell }: EstimateSwapGasLimitArgs) => - createQueryKey('estimateSwapGasLimit', { chainId, quote, assetToSell }, { persisterVersion: 1 }); + createQueryKey('estimateSwapGasLimit', { chainId, quote, assetToSell }); type EstimateSwapGasLimitQueryKey = ReturnType; @@ -93,6 +93,13 @@ export function useSwapEstimatedGasLimit( assetToSell, }), estimateSwapGasLimitQueryFunction, - { keepPreviousData: true, staleTime: 12000, cacheTime: Infinity, ...config } + { + staleTime: 30 * 1000, // 30s + cacheTime: 60 * 1000, // 1min + notifyOnChangeProps: ['data', 'isFetching'], + keepPreviousData: true, + placeholderData: gasUnits.basic_swap[chainId], + ...config, + } ); } diff --git a/src/__swaps__/utils/meteorology.ts b/src/__swaps__/utils/meteorology.ts index 3606135cd51..cff7bc1b5f3 100644 --- a/src/__swaps__/utils/meteorology.ts +++ b/src/__swaps__/utils/meteorology.ts @@ -5,7 +5,9 @@ import { rainbowMeteorologyGetData } from '@/handlers/gasFees'; import { abs, lessThan, subtract } from '@/helpers/utilities'; import { QueryConfig, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; import { getNetworkFromChainId } from '@/utils/ethereumUtils'; -import { GasSpeed, getGasSettings, getSelectedGasSpeed } from '../screens/Swap/hooks/useSelectedGas'; +import { useCallback } from 'react'; +import { GasSettings } from '../screens/Swap/hooks/useCustomGas'; +import { GasSpeed, getSelectedGasSpeed, useGasSettings } from '../screens/Swap/hooks/useSelectedGas'; import { getMinimalTimeUnitStringForMs } from './time'; // Query Types @@ -89,7 +91,11 @@ export async function fetchMeteorology( export function useMeteorology( { chainId }: MeteorologyArgs, - { select, enabled }: { select?: (data: MeteorologyResult) => Selected; enabled?: boolean } = { select: data => data as Selected } + { + select, + enabled, + notifyOnChangeProps = ['data'], + }: { select?: (data: MeteorologyResult) => Selected; enabled?: boolean; notifyOnChangeProps?: 'data'[] } ) { return useQuery(meteorologyQueryKey({ chainId }), meteorologyQueryFunction, { select, @@ -97,7 +103,7 @@ export function useMeteorology( refetchInterval: 12_000, // 12 seconds staleTime: 12_000, // 12 seconds cacheTime: Infinity, - notifyOnChangeProps: ['data'], + notifyOnChangeProps, }); } @@ -186,19 +192,21 @@ function findClosestValue(target: string, array: string[]) { })!; } +function selectEstimatedTime({ data }: MeteorologyResult, selectedGas: GasSettings | undefined) { + if ('legacy' in data) return undefined; + if (!selectedGas?.isEIP1559) return undefined; + const value = findClosestValue(selectedGas.maxPriorityFee, Object.values(data.confirmationTimeByPriorityFee)); + const [time] = Object.entries(data.confirmationTimeByPriorityFee).find(([, v]) => v === value) || []; + if (!time) return undefined; + return `${+time >= 3600 ? '>' : '~'} ${getMinimalTimeUnitStringForMs(+time * 1000)}`; +} + export function useEstimatedTime({ chainId, speed }: { chainId: ChainId; speed: GasSpeed }) { + const selectedGas = useGasSettings(chainId, speed); return useMeteorology( { chainId }, { - select: ({ data }) => { - if ('legacy' in data) return undefined; - const gasSettings = getGasSettings(speed, chainId); - if (!gasSettings?.isEIP1559) return undefined; - const value = findClosestValue(gasSettings.maxPriorityFee, Object.values(data.confirmationTimeByPriorityFee)); - const [time] = Object.entries(data.confirmationTimeByPriorityFee).find(([, v]) => v === value) || []; - if (!time) return undefined; - return `${+time >= 3600 ? '>' : '~'} ${getMinimalTimeUnitStringForMs(+time * 1000)}`; - }, + select: useCallback((data: MeteorologyResult) => selectEstimatedTime(data, selectedGas), [selectedGas]), } ); } @@ -209,20 +217,40 @@ export const getCachedCurrentBaseFee = (chainId: ChainId) => { return selectBaseFee(data); }; -export function useMeteorologySuggestions>({ +type GasSuggestions = ReturnType; +export function useMeteorologySuggestions({ chainId, enabled }: { chainId: ChainId; enabled?: boolean }) { + return useMeteorology({ chainId }, { select: selectGasSuggestions, enabled }); +} + +export function useMeteorologySuggestion({ chainId, + speed, enabled, select = s => s as Selected, + notifyOnChangeProps = ['data'], }: { chainId: ChainId; + speed: GasSpeed; enabled?: boolean; - select?: (d: ReturnType) => Selected; + select?: (d: GasSuggestions[keyof GasSuggestions] | undefined) => Selected; + notifyOnChangeProps?: ['data'] | []; }) { - return useMeteorology({ chainId }, { select: d => select(selectGasSuggestions(d)), enabled }); + return useMeteorology( + { chainId }, + { + select: useCallback( + (d: MeteorologyResult) => select(speed === 'custom' ? undefined : selectGasSuggestions(d)[speed]), + [select, speed] + ), + enabled: enabled && speed !== 'custom', + notifyOnChangeProps, + } + ); } +const selectIsEIP1559 = ({ data }: MeteorologyResult) => !('legacy' in data); export const useIsChainEIP1559 = (chainId: ChainId) => { - const { data } = useMeteorology({ chainId }, { select: ({ data }) => !('legacy' in data) }); + const { data } = useMeteorology({ chainId }, { select: selectIsEIP1559 }); if (data === undefined) return true; return data; }; diff --git a/src/hooks/useWhyDidYouUpdate.ts b/src/hooks/useWhyDidYouUpdate.ts new file mode 100644 index 00000000000..032e778e6ee --- /dev/null +++ b/src/hooks/useWhyDidYouUpdate.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useRef } from 'react'; + +/** + * Debug hook showing which props updated between two renders + * @example + * + * const MyComponent = React.memo(props => { + * useWhyDidYouUpdate('MyComponent', props); + * return ) { + // Get a mutable ref object where we can store props ... + // ... for comparison next time this hook runs. + const previousProps = useRef() as any; + + useEffect(() => { + if (previousProps.current) { + // Get all keys from previous and current props + const allKeys = Object.keys({ ...previousProps.current, ...props }); + // Use this object to keep track of changed props + const changesObj: Record = {}; + // Iterate through keys + allKeys.forEach(key => { + // If previous is different from current + if (previousProps.current[key] !== props[key]) { + // Add to changesObj + changesObj[key] = { + from: previousProps.current[key], + to: props[key], + }; + } + }); + + // If changesObj not empty then output to console + if (Object.keys(changesObj).length) { + console.log('[why-did-you-update]', name, changesObj); + } + } + + // Finally update previousProps with current props for next hook call + previousProps.current = props; + }); +}