From a56788dd0a040ccd231d32862a11de36c2f990c6 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Mon, 22 Dec 2025 14:49:17 +0800 Subject: [PATCH] feat: add usePriceQuery hook with TanStack Query - Create usePriceQuery hook with: - 5min staleTime: replaces manual cache logic - 60s polling: automatic price refresh - Shared cache across components - Request deduplication - Keep existing usePriceService for backward compatibility Closes #45 --- src/queries/index.ts | 7 +++ src/queries/use-price-query.ts | 89 ++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/queries/use-price-query.ts diff --git a/src/queries/index.ts b/src/queries/index.ts index e2d16d379..7fd2b7a4e 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -13,3 +13,10 @@ export { useRefreshBalance, balanceQueryKeys, } from './use-balance-query' + +export { + usePriceQuery, + getPrice, + priceQueryKeys, + type PriceData, +} from './use-price-query' diff --git a/src/queries/use-price-query.ts b/src/queries/use-price-query.ts new file mode 100644 index 000000000..e05f9c0b8 --- /dev/null +++ b/src/queries/use-price-query.ts @@ -0,0 +1,89 @@ +import { useQuery } from '@tanstack/react-query' + +/** Price data for a single token */ +export interface PriceData { + priceUsd: number + priceChange24h: number + updatedAt: number +} + +/** Mock price data for development */ +const MOCK_PRICES: Record = { + ETH: { priceUsd: 2500, priceChange24h: 2.3, updatedAt: Date.now() }, + BTC: { priceUsd: 45000, priceChange24h: -1.5, updatedAt: Date.now() }, + USDT: { priceUsd: 1, priceChange24h: 0.01, updatedAt: Date.now() }, + USDC: { priceUsd: 1, priceChange24h: -0.02, updatedAt: Date.now() }, + BFM: { priceUsd: 0.05, priceChange24h: 5.2, updatedAt: Date.now() }, + TRX: { priceUsd: 0.12, priceChange24h: 3.1, updatedAt: Date.now() }, + BNB: { priceUsd: 320, priceChange24h: 1.8, updatedAt: Date.now() }, +} + +/** + * Price Query Keys + */ +export const priceQueryKeys = { + all: ['prices'] as const, + symbols: (symbols: string[]) => ['prices', symbols.sort().join(',')] as const, +} + +/** + * Fetch prices for given symbols + */ +async function fetchPrices(symbols: string[]): Promise> { + const now = Date.now() + + // TODO: Replace with actual API call (CoinGecko, etc.) + await new Promise((resolve) => setTimeout(resolve, 100)) + + const prices = new Map() + for (const symbol of symbols) { + const upperSymbol = symbol.toUpperCase() + const mockPrice = MOCK_PRICES[upperSymbol] + if (mockPrice) { + prices.set(upperSymbol, { ...mockPrice, updatedAt: now }) + } + } + + return prices +} + +/** + * Price Query Hook + * + * 特性: + * - 5min staleTime:替代手动缓存 + * - 60s 轮询:自动刷新价格 + * - 共享缓存:多个组件使用同一 symbols 时共享数据 + * - 请求去重:同时发起的相同请求会被合并 + */ +export function usePriceQuery(symbols: string[]) { + const normalizedSymbols = symbols.map((s) => s.toUpperCase()).filter(Boolean) + + const query = useQuery({ + queryKey: priceQueryKeys.symbols(normalizedSymbols), + queryFn: () => fetchPrices(normalizedSymbols), + enabled: normalizedSymbols.length > 0, + staleTime: 5 * 60 * 1000, // 5 分钟内认为数据新鲜 + gcTime: 10 * 60 * 1000, // 10 分钟缓存 + refetchInterval: 60 * 1000, // 60 秒轮询 + refetchIntervalInBackground: false, + refetchOnWindowFocus: true, + }) + + return { + prices: query.data ?? new Map(), + isLoading: query.isLoading, + isFetching: query.isFetching, + error: query.error?.message ?? null, + } +} + +/** + * Get price for a single token from the prices map + */ +export function getPrice( + prices: Map, + symbol: string +): PriceData | undefined { + return prices.get(symbol.toUpperCase()) +}