Installation
Copy
Ask AI
bun add @tanstack/react-query @tevm/voltaire
Setup
Wrap your app withQueryClientProvider:
Copy
Ask AI
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
)
}
Basic Usage
Fetching Address Balance
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
// 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:Copy
Ask AI
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:Copy
Ask AI
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:Copy
Ask AI
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Best Practices
Cache Configuration
- Use
staleTime: Infinityfor immutable data (confirmed transactions, historical blocks) - Use short
staleTime(5-15s) for frequently changing data (balances, pending state) - Use
refetchIntervalfor data that changes on a known schedule (block numbers)
Rate LimitingPublic RPC endpoints have rate limits. Consider:
- Batching requests where possible
- Using
staleTimeto reduce refetches - Implementing request deduplication with query keys
Next Steps
- JSONRPCProvider - Full provider documentation
- Address - Address primitive reference
- Uint - Number handling utilities

