Data fetching
Feed live balances, prices, and portfolios into onchain-ui components with TanStack Query.
AssetRow and TokenBalance display the values you pass as props. This recipe uses TanStack Query to fetch and cache those values. If you use wagmi, you can reuse its QueryClientProvider.
Portfolio query
The example expects /api/portfolio/{address} to return holdings shaped like AssetRow props. Implement that endpoint in your app. placeholderData: keepPreviousData keeps the previous wallet's holdings visible while the new wallet's request runs. Remove it to show a loading state when the new wallet has no cached data.
"use client"
import { keepPreviousData, useQuery } from "@tanstack/react-query"
import { AssetRow } from "@/components/ui/asset-row"
import type { Address } from "viem"
type Holding = {
symbol: string
name: string
src: string | null
amount: number
value: number
change: number
chainId: number
}
async function fetchPortfolio(address: Address): Promise<Holding[]> {
const res = await fetch(`/api/portfolio/${address}`)
if (!res.ok) throw new Error("Failed to load portfolio")
return res.json()
}
export function Portfolio({ address }: { address: Address }) {
const { data: holdings, isPending } = useQuery({
queryKey: ["portfolio", address],
queryFn: () => fetchPortfolio(address),
staleTime: 30_000,
refetchInterval: 60_000,
placeholderData: keepPreviousData,
})
if (isPending) {
return (
<div className="grid gap-2">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-16 animate-pulse rounded-lg border bg-muted" />
))}
</div>
)
}
return (
<div className="grid gap-2">
{holdings?.map((holding) => (
<AssetRow key={`${holding.chainId}:${holding.symbol}`} {...holding} />
))}
</div>
)
}Cache windows
These cache windows are starting points for this recipe. Adjust them for your API's rate limits and how often the displayed data needs to update.
| Data | staleTime | refetchInterval |
|---|---|---|
| Token prices | 30s | 60s |
| Wallet balances | 15 to 30s | 30 to 60s |
| ENS names and Basenames | 24h | None |
| Historical chart data | 5m+ | None |
| Token names, decimals, and logos | 24h+ | None |
When you configure a query:
- Set
staleTimeto control how long TanStack Query considers a result fresh. SetrefetchIntervalto poll on a schedule, independently ofstaleTime. Invalidate balance queries after a transaction to request updated balances. - Include every input that changes the result in the query key. Use
["portfolio", address, chainId]if your endpoint filters by both wallet and chain. Each combination then has its own cache entry.
AddressIdentity caches name and avatar lookups for the session. Use resolveOnchainIdentity from lib/onchain/resolvers in a query if other parts of your app need the result. The resolver still uses its session cache, so a query refetch alone does not force a new RPC lookup.
Live prices into TokenPrice
This example polls your /api/prices/eth endpoint every 60 seconds and passes the result to TokenPrice:
"use client"
import { useQuery } from "@tanstack/react-query"
import { TokenPrice } from "@/components/ui/token-price"
async function fetchEthPrice(): Promise<{ price: number; change24h: number }> {
const res = await fetch("/api/prices/eth")
if (!res.ok) throw new Error("Failed to load price")
return res.json()
}
export function EthPrice() {
const { data } = useQuery({
queryKey: ["price", "eth"],
queryFn: fetchEthPrice,
staleTime: 30_000,
refetchInterval: 60_000,
})
return <TokenPrice value={data?.price ?? null} change={data?.change24h} />
}While value is null, TokenPrice shows its fallback, which defaults to --.