Skip to main content
TanStack Query (React Query) integrates seamlessly with Tevm for fetching and caching Ethereum data in React applications.

Installation

bun add @tanstack/react-query @tevm/voltaire

Setup

Wrap your app with QueryClientProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  )
}

Basic Usage

Fetching Address Balance

import { useQuery } from '@tanstack/react-query'
import { Address } from '@tevm/voltaire/Address'
import { Wei } from '@tevm/voltaire/Denomination'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

function useBalance(address: string) {
  return useQuery({
    queryKey: ['balance', address],
    queryFn: async () => {
      const addr = Address.from(address)
      const balance = await provider.eth.getBalance({ address: addr })
      return Wei.toEther(balance)
    },
    staleTime: 10_000, // 10 seconds
  })
}

function BalanceDisplay({ address }: { address: string }) {
  const { data: balance, isLoading, error } = useBalance(address)

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return <div>Balance: {balance} ETH</div>
}

Fetching Block Data

import { useQuery } from '@tanstack/react-query'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

function useBlock(blockNumber?: bigint | 'latest') {
  return useQuery({
    queryKey: ['block', blockNumber?.toString() ?? 'latest'],
    queryFn: () => provider.eth.getBlockByNumber({
      blockNumber: blockNumber ?? 'latest',
      includeTransactions: false,
    }),
    staleTime: blockNumber === 'latest' ? 12_000 : Infinity,
  })
}

Transaction History

import { useQuery } from '@tanstack/react-query'
import { Address } from '@tevm/voltaire/Address'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

function useTransactionCount(address: string) {
  return useQuery({
    queryKey: ['txCount', address],
    queryFn: async () => {
      const addr = Address.from(address)
      return provider.eth.getTransactionCount({ address: addr })
    },
  })
}

Advanced Patterns

Parallel Queries

Fetch multiple addresses simultaneously:
import { useQueries } from '@tanstack/react-query'
import { Address } from '@tevm/voltaire/Address'
import { Wei } from '@tevm/voltaire/Denomination'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

function useMultipleBalances(addresses: string[]) {
  return useQueries({
    queries: addresses.map(address => ({
      queryKey: ['balance', address],
      queryFn: async () => {
        const addr = Address.from(address)
        const balance = await provider.eth.getBalance({ address: addr })
        return { address, balance: Wei.toEther(balance) }
      },
    })),
  })
}

Dependent Queries

Chain queries when one depends on another:
import { useQuery } from '@tanstack/react-query'
import { Address } from '@tevm/voltaire/Address'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

function useContractCode(address: string) {
  const { data: code } = useQuery({
    queryKey: ['code', address],
    queryFn: async () => {
      const addr = Address.from(address)
      return provider.eth.getCode({ address: addr })
    },
  })

  return useQuery({
    queryKey: ['isContract', address],
    queryFn: () => code && code.length > 2,
    enabled: !!code,
  })
}

Polling for Updates

Poll for block updates:
import { useQuery } from '@tanstack/react-query'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider('https://eth.llamarpc.com')

function useLatestBlock() {
  return useQuery({
    queryKey: ['latestBlock'],
    queryFn: () => provider.eth.getBlockNumber(),
    refetchInterval: 12_000, // Poll every 12 seconds (block time)
  })
}

Optimistic Updates

Update cache optimistically when sending transactions:
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Address } from '@tevm/voltaire/Address'
import { Wei } from '@tevm/voltaire/Denomination'

function useSendTransaction() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async (tx: { to: string; value: bigint }) => {
      // Your transaction sending logic
    },
    onMutate: async ({ to, value }) => {
      await queryClient.cancelQueries({ queryKey: ['balance', to] })

      const previousBalance = queryClient.getQueryData(['balance', to])

      queryClient.setQueryData(['balance', to], (old?: string) => {
        const oldEther = old ? BigInt(old) : 0n
        const oldWei = Wei.fromEther(oldEther)
        const newWei = oldWei + value
        return Wei.toEther(newWei)
      })

      return { previousBalance }
    },
    onError: (err, variables, context) => {
      queryClient.setQueryData(['balance', variables.to], context?.previousBalance)
    },
    onSettled: (data, error, variables) => {
      queryClient.invalidateQueries({ queryKey: ['balance', variables.to] })
    },
  })
}

Query Key Patterns

Structure query keys for efficient cache management:
const queryKeys = {
  // Address-specific
  balance: (address: string) => ['balance', address] as const,
  nonce: (address: string) => ['nonce', address] as const,
  code: (address: string) => ['code', address] as const,

  // Block-specific
  block: (number: bigint | 'latest') => ['block', number.toString()] as const,
  blockByHash: (hash: string) => ['block', 'hash', hash] as const,

  // Transaction-specific
  tx: (hash: string) => ['tx', hash] as const,
  txReceipt: (hash: string) => ['txReceipt', hash] as const,

  // Contract state
  storage: (address: string, slot: string) => ['storage', address, slot] as const,
}

Custom Hooks Library

Create a reusable hooks library:
// hooks/useEthereum.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Address } from '@tevm/voltaire/Address'
import { Wei } from '@tevm/voltaire/Denomination'
import { createJsonRpcProvider } from '@tevm/voltaire/jsonrpc'

const provider = createJsonRpcProvider(process.env.NEXT_PUBLIC_RPC_URL!)

export function useBalance(address: string | undefined) {
  return useQuery({
    queryKey: ['balance', address],
    queryFn: async () => {
      if (!address) throw new Error('No address')
      const addr = Address.from(address)
      const balance = await provider.eth.getBalance({ address: addr })
      return Wei.toEther(balance)
    },
    enabled: !!address,
    staleTime: 10_000,
  })
}

export function useBlockNumber() {
  return useQuery({
    queryKey: ['blockNumber'],
    queryFn: () => provider.eth.getBlockNumber(),
    refetchInterval: 12_000,
  })
}

export function useGasPrice() {
  return useQuery({
    queryKey: ['gasPrice'],
    queryFn: () => provider.eth.getGasPrice(),
    staleTime: 5_000,
  })
}

Error Handling

Handle RPC errors gracefully:
import { useQuery } from '@tanstack/react-query'

function useBalanceWithRetry(address: string) {
  return useQuery({
    queryKey: ['balance', address],
    queryFn: async () => {
      // Your fetch logic
    },
    retry: (failureCount, error) => {
      // Retry on network errors, not on validation errors
      if (error.message.includes('invalid address')) return false
      return failureCount < 3
    },
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
  })
}

React Suspense Integration

Use with React Suspense:
import { useSuspenseQuery } from '@tanstack/react-query'
import { Suspense } from 'react'

function Balance({ address }: { address: string }) {
  const { data } = useSuspenseQuery({
    queryKey: ['balance', address],
    queryFn: async () => {
      // Your fetch logic
    },
  })

  return <div>{data} ETH</div>
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Balance address="0x..." />
    </Suspense>
  )
}

DevTools

Enable React Query DevTools for debugging:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

Best Practices

Cache Configuration
  • Use staleTime: Infinity for immutable data (confirmed transactions, historical blocks)
  • Use short staleTime (5-15s) for frequently changing data (balances, pending state)
  • Use refetchInterval for data that changes on a known schedule (block numbers)
Rate LimitingPublic RPC endpoints have rate limits. Consider:
  • Batching requests where possible
  • Using staleTime to reduce refetches
  • Implementing request deduplication with query keys

Next Steps