onchain-ui
Recipes

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.

DatastaleTimerefetchInterval
Token prices30s60s
Wallet balances15 to 30s30 to 60s
ENS names and Basenames24hNone
Historical chart data5m+None
Token names, decimals, and logos24h+None

When you configure a query:

  • Set staleTime to control how long TanStack Query considers a result fresh. Set refetchInterval to poll on a schedule, independently of staleTime. 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 --.

On this page