Skip to main content

Documentation Index

Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt

Use this file to discover all available pages before exploring further.

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