Sitemap
mirbasit01

mirbasit01

Front-End Developer | React.js | Next.js | TypeScript | Tailwind CSS | Web3.js | Smart Contracts | Creating responsive & innovative web apps

What is Wagmi v2 and Why Use It?

15 min readAug 8, 2025

--

Press enter or click to view image in full size

Published by mirbasit01

Wagmi v2: The Complete Guide to Modern Web3 Development

Wagmi v2 is a powerful React hooks library designed for interacting with Ethereum and other EVM-compatible blockchains. It simplifies Web3 development by providing a collection of React hooks that handle wallet connections, smart contract interactions, and blockchain data fetching.

Why Choose Wagmi v2?

  1. Type Safety: Built with TypeScript for better development experience and fewer runtime errors
  2. React Integration: Follows React hooks patterns for seamless integration
  3. Multi-chain Support: Works with Ethereum, Polygon, Arbitrum, Optimism, and more
  4. Automatic Caching: Efficient data caching and synchronization
  5. Developer Experience: Excellent debugging tools and error handling
  6. Performance: Optimized for production applications

Supported Languages and Frameworks

Primary Languages:

  • TypeScript (Recommended)
  • JavaScript (ES6+)

Frontend Frameworks:

  • React (v16.8+)
  • Next.js
  • Vite
  • Create React App
  • Remix

Compatible Environments:

  • Node.js environments
  • Modern bundlers (Webpack, Rollup, esbuild, SWC)
  • Server-side rendering (SSR)

Installation and Setup

# NPM
npm install wagmi viem @tanstack/react-query
# Yarn
yarn add wagmi viem @tanstack/react-query
# PNPM
pnpm add wagmi viem @tanstack/react-query

Additional Wallet Connectors (Optional)

# For popular wallet connections
npm install @wagmi/connectors

Basic Configuration

import { WagmiProvider, createConfig, http } from 'wagmi'
import { mainnet, polygon, arbitrum, base } from 'wagmi/chains'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { injected, metaMask, walletConnect } from '@wagmi/connectors'
// Configure supported chains and transports
const config = createConfig({
chains: [mainnet, polygon, arbitrum, base],
connectors: [
injected(),
metaMask(),
walletConnect({
projectId: 'your-walletconnect-project-id'
})
],
transports: {
[mainnet.id]: http(),
[polygon.id]: http(),
[arbitrum.id]: http(),
[base.id]: http(),
},
})
// Create query client
const queryClient = new QueryClient()
function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
</WagmiProvider>
)
}
export default App

Essential Wallet Connection Hooks

useConnect Hook

The useConnect hook handles wallet connections with multiple connector support:

import { useConnect } from 'wagmi'
function ConnectWallet() {
const { connectors, connect, status, error } = useConnect()
return (
<div className="wallet-connect">
<h2>Connect Your Wallet</h2>
<div className="connector-buttons">
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => connect({ connector })}
disabled={status === 'pending'}
className="connector-btn"
>
<img src={connector.icon} alt={connector.name} />
{connector.name}
</button>
))}
</div>

{status === 'pending' && <p>Connecting to wallet...</p>}
{error && (
<div className="error">
<p>Connection failed: {error.message}</p>
</div>
)}
</div>
)
}

useAccount Hook

Manages connected account information:

import { useAccount, useDisconnect, useEnsName } from 'wagmi'
function AccountInfo() {
const { address, isConnected, chain } = useAccount()
const { disconnect } = useDisconnect()
const { data: ensName } = useEnsName({ address })
if (!isConnected) {
return <ConnectWallet />
}
return (
<div className="account-info">
<div className="account-details">
<h3>Connected Account</h3>
<p><strong>Address:</strong> {address}</p>
{ensName && <p><strong>ENS:</strong> {ensName}</p>}
<p><strong>Network:</strong> {chain?.name}</p>
</div>

<button onClick={() => disconnect()} className="disconnect-btn">
Disconnect Wallet
</button>
</div>
)
}

useBalance Hook

Fetch account balances:

import { useBalance } from 'wagmi'
import { formatEther } from 'viem'
function WalletBalance({ address }: { address: string }) {
const {
data: balance,
isLoading,
error,
refetch
} = useBalance({
address: address as `0x${string}`,
})
if (isLoading) return <div>Loading balance...</div>
if (error) return <div>Error fetching balance</div>
return (
<div className="balance-info">
<h4>Wallet Balance</h4>
<p>
{balance ? formatEther(balance.value) : '0'} {balance?.symbol}
</p>
<button onClick={() => refetch()}>Refresh Balance</button>
</div>
)
}

Smart Contract Read Operations

useReadContract Hook

Read data from smart contracts:

import { useReadContract } from 'wagmi'
// ERC20 Token ABI (partial)
const ERC20_ABI = [
{
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'owner', type: 'address' }],
outputs: [{ name: 'balance', type: 'uint256' }],
},
{
name: 'symbol',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [{ name: 'symbol', type: 'string' }],
},
{
name: 'decimals',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [{ name: 'decimals', type: 'uint8' }],
}
] as const
function TokenBalance({ tokenAddress, userAddress }: {
tokenAddress: string
userAddress: string
}) {
// Read token balance
const { data: balance, isLoading: balanceLoading } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [userAddress as `0x${string}`],
})
// Read token symbol
const { data: symbol } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'symbol',
})
// Read token decimals
const { data: decimals } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'decimals',
})
if (balanceLoading) return <div>Loading token balance...</div> const formatBalance = (balance: bigint, decimals: number) => {
return (Number(balance) / Math.pow(10, decimals)).toFixed(4)
}
return (
<div className="token-balance">
<h4>{symbol} Balance</h4>
<p>
{balance && decimals
? formatBalance(balance, decimals)
: '0'} {symbol}
</p>
</div>
)
}

useReadContracts Hook (Multiple Reads)

Read from multiple contracts or functions simultaneously:

import { useReadContracts } from 'wagmi'
function TokenInfo({ tokenAddress }: { tokenAddress: string }) {
const { data, isLoading, error } = useReadContracts({
contracts: [
{
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'name',
},
{
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'symbol',
},
{
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'totalSupply',
},
],
})
if (isLoading) return <div>Loading token info...</div>
if (error) return <div>Error: {error.message}</div>
const [name, symbol, totalSupply] = data || [] return (
<div className="token-info">
<h3>Token Information</h3>
<p><strong>Name:</strong> {name?.result}</p>
<p><strong>Symbol:</strong> {symbol?.result}</p>
<p><strong>Total Supply:</strong> {totalSupply?.result?.toString()}</p>
</div>
)
}

Smart Contract Write Operations

useWriteContract Hook

Execute transactions and write to smart contracts:

import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { parseEther } from 'viem'
import { useState } from 'react'
const TRANSFER_ABI = [
{
name: 'transfer',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ name: 'success', type: 'bool' }],
},
] as const
function TokenTransfer({ tokenAddress }: { tokenAddress: string }) {
const [recipient, setRecipient] = useState('')
const [amount, setAmount] = useState('')
const {
writeContract,
data: hash,
error: writeError,
isPending: isWritePending
} = useWriteContract()
const {
isLoading: isConfirming,
isSuccess: isConfirmed,
error: receiptError
} = useWaitForTransactionReceipt({
hash,
})
const handleTransfer = async () => {
if (!recipient || !amount) {
alert('Please fill in all fields')
return
}
try {
writeContract({
address: tokenAddress as `0x${string}`,
abi: TRANSFER_ABI,
functionName: 'transfer',
args: [
recipient as `0x${string}`,
parseEther(amount)
],
})
} catch (error) {
console.error('Transfer error:', error)
}
}
return (
<div className="token-transfer">
<h3>Transfer Tokens</h3>

<div className="transfer-form">
<div className="form-group">
<label>Recipient Address:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="0x..."
/>
</div>

<div className="form-group">
<label>Amount:</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
step="0.01"
/>
</div>

<button
onClick={handleTransfer}
disabled={isWritePending || isConfirming}
className="transfer-btn"
>
{isWritePending ? 'Preparing...' :
isConfirming ? 'Confirming...' : 'Transfer'}
</button>
</div>
{hash && (
<div className="transaction-status">
<p><strong>Transaction Hash:</strong> {hash}</p>
{isConfirming && <p>⏳ Waiting for confirmation...</p>}
{isConfirmed && <p>✅ Transaction confirmed!</p>}
</div>
)}
{(writeError || receiptError) && (
<div className="error">
<p>❌ Error: {(writeError || receiptError)?.message}</p>
</div>
)}
</div>
)
}

useSimulateContract Hook

Simulate contract calls before execution:

import { useSimulateContract, useWriteContract } from 'wagmi'
import { parseEther } from 'viem'
function SafeTokenTransfer({ tokenAddress }: { tokenAddress: string }) {
const [recipient, setRecipient] = useState('')
const [amount, setAmount] = useState('')
// Simulate the transaction first
const {
data: simulateData,
error: simulateError,
isLoading: isSimulating
} = useSimulateContract({
address: tokenAddress as `0x${string}`,
abi: TRANSFER_ABI,
functionName: 'transfer',
args: recipient && amount ? [
recipient as `0x${string}`,
parseEther(amount)
] : undefined,
query: {
enabled: !!(recipient && amount),
},
})
const { writeContract, isPending } = useWriteContract() const handleTransfer = () => {
if (simulateData?.request) {
writeContract(simulateData.request)
}
}
return (
<div>
{/* Form inputs */}
<input
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="Recipient address"
/>
<input
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>

<button
onClick={handleTransfer}
disabled={isPending || isSimulating || !!simulateError}
>
{isSimulating ? 'Validating...' :
isPending ? 'Transferring...' : 'Transfer'}
</button>
{simulateError && (
<p className="error">
Transaction will fail: {simulateError.message}
</p>
)}
</div>
)
}

Advanced Hooks and Features

useBlockNumber Hook

Track current block number:

import { useBlockNumber } from 'wagmi'
function BlockTracker() {
const { data: blockNumber, isLoading } = useBlockNumber({
watch: true, // Subscribe to new blocks
})
return (
<div className="block-tracker">
<h4>Current Block</h4>
<p>#{blockNumber?.toString() || 'Loading...'}</p>
</div>
)
}

useTransaction Hook

Get transaction details:

import { useTransaction } from 'wagmi'
function TransactionDetails({ hash }: { hash: string }) {
const { data: transaction, isLoading, error } = useTransaction({
hash: hash as `0x${string}`,
})
if (isLoading) return <div>Loading transaction...</div>
if (error) return <div>Error: {error.message}</div>
if (!transaction) return <div>Transaction not found</div>
return (
<div className="transaction-details">
<h4>Transaction Details</h4>
<p><strong>From:</strong> {transaction.from}</p>
<p><strong>To:</strong> {transaction.to}</p>
<p><strong>Value:</strong> {transaction.value.toString()} wei</p>
<p><strong>Gas Used:</strong> {transaction.gas.toString()}</p>
<p><strong>Block:</strong> {transaction.blockNumber?.toString()}</p>
</div>
)
}

useWatchContractEvent Hook

Listen to contract events:

import { useWatchContractEvent } from 'wagmi'
const TRANSFER_EVENT_ABI = [
{
type: 'event',
name: 'Transfer',
inputs: [
{ name: 'from', type: 'address', indexed: true },
{ name: 'to', type: 'address', indexed: true },
{ name: 'value', type: 'uint256', indexed: false }
],
},
] as const
function TransferEventListener({ tokenAddress }: { tokenAddress: string }) {
const [events, setEvents] = useState<any[]>([])
useWatchContractEvent({
address: tokenAddress as `0x${string}`,
abi: TRANSFER_EVENT_ABI,
eventName: 'Transfer',
onLogs: (logs) => {
console.log('New transfer events:', logs)
setEvents(prev => [...logs, ...prev].slice(0, 10)) // Keep last 10
},
})
return (
<div className="event-listener">
<h4>Recent Transfers</h4>
{events.length === 0 ? (
<p>No recent transfers</p>
) : (
<ul>
{events.map((event, index) => (
<li key={index}>
Transfer: {event.args.value.toString()} tokens
from {event.args.from} to {event.args.to}
</li>
))}
</ul>
)}
</div>
)
}

Complete Example: DeFi Token Manager

Here’s a comprehensive example that combines multiple Wagmi v2 features:

import React, { useState } from 'react'
import {
useAccount,
useConnect,
useDisconnect,
useBalance,
useReadContract,
useWriteContract,
useWaitForTransactionReceipt,
useWatchContractEvent
} from 'wagmi'
import { formatEther, parseEther } from 'viem'
// Complete ERC20 ABI
const ERC20_ABI = [
// Read functions
{ name: 'name', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] },
{ name: 'symbol', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] },
{ name: 'decimals', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint8' }] },
{ name: 'totalSupply', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256' }] },
{ name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] },

// Write functions
{ name: 'transfer', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ name: '', type: 'bool' }] },
{ name: 'approve', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ name: '', type: 'bool' }] },

// Events
{ type: 'event', name: 'Transfer', inputs: [{ name: 'from', type: 'address', indexed: true }, { name: 'to', type: 'address', indexed: true }, { name: 'value', type: 'uint256', indexed: false }] },
] as const
function TokenManager() {
const [tokenAddress, setTokenAddress] = useState('0x..') // Your token address
const [recipient, setRecipient] = useState('')
const [amount, setAmount] = useState('')
const [events, setEvents] = useState<any[]>([])
// Wallet connection
const { address, isConnected } = useAccount()
const { connectors, connect } = useConnect()
const { disconnect } = useDisconnect()
// Balances
const { data: ethBalance } = useBalance({ address })
const { data: tokenBalance } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: address ? [address] : undefined,
query: { enabled: !!address }
})
// Token info
const { data: tokenName } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'name',
})

const { data: tokenSymbol } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'symbol',
})
const { data: tokenDecimals } = useReadContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'decimals',
})
// Write contract
const { writeContract, data: hash, isPending } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })
// Event listener
useWatchContractEvent({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
eventName: 'Transfer',
onLogs: (logs) => {
setEvents(prev => [...logs, ...prev].slice(0, 5))
},
})
const handleTransfer = () => {
if (!recipient || !amount || !tokenDecimals) return

writeContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'transfer',
args: [
recipient as `0x${string}`,
parseEther(amount) // Adjust based on token decimals
],
})
}
const formatTokenBalance = (balance: bigint | undefined, decimals: number | undefined) => {
if (!balance || !decimals) return '0'
return (Number(balance) / Math.pow(10, decimals)).toFixed(4)
}
if (!isConnected) {
return (
<div className="connect-section">
<h1>Token Manager</h1>
<p>Connect your wallet to start managing tokens</p>
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => connect({ connector })}
className="connect-btn"
>
Connect {connector.name}
</button>
))}
</div>
)
}
return (
<div className="token-manager">
<header className="app-header">
<h1>Token Manager</h1>
<div className="account-info">
<p><strong>Address:</strong> {address}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
</header>
<div className="dashboard">
{/* Balances Section */}
<section className="balances">
<h2>Balances</h2>
<div className="balance-cards">
<div className="balance-card">
<h3>ETH Balance</h3>
<p>{ethBalance ? formatEther(ethBalance.value) : '0'} ETH</p>
</div>
<div className="balance-card">
<h3>{tokenSymbol || 'Token'} Balance</h3>
<p>
{formatTokenBalance(tokenBalance, tokenDecimals)} {tokenSymbol}
</p>
</div>
</div>
</section>
{/* Token Info Section */}
<section className="token-info">
<h2>Token Information</h2>
<div className="token-details">
<p><strong>Name:</strong> {tokenName || 'Loading...'}</p>
<p><strong>Symbol:</strong> {tokenSymbol || 'Loading...'}</p>
<p><strong>Decimals:</strong> {tokenDecimals || 'Loading...'}</p>
</div>
</section>
{/* Transfer Section */}
<section className="transfer">
<h2>Transfer Tokens</h2>
<div className="transfer-form">
<input
type="text"
placeholder="Recipient address"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
/>
<input
type="number"
placeholder="Amount"
value={amount}
onChange={(e) => setAmount(e.target.value)}
step="0.01"
/>
<button
onClick={handleTransfer}
disabled={isPending || isConfirming || !recipient || !amount}
>
{isPending ? 'Preparing...' :
isConfirming ? 'Confirming...' : 'Transfer'}
</button>
</div>
{hash && (
<div className="transaction-status">
<p><strong>Transaction:</strong> {hash}</p>
{isConfirming && <p>⏳ Confirming...</p>}
{isSuccess && <p>✅ Transfer completed!</p>}
</div>
)}
</section>
{/* Recent Events */}
<section className="events">
<h2>Recent Transfers</h2>
{events.length === 0 ? (
<p>No recent transfers</p>
) : (
<div className="event-list">
{events.map((event, index) => (
<div key={index} className="event-item">
<p>
<strong>Transfer:</strong> {formatTokenBalance(event.args.value, tokenDecimals)} {tokenSymbol}
</p>
<p><strong>From:</strong> {event.args.from}</p>
<p><strong>To:</strong> {event.args.to}</p>
</div>
))}
</div>
)}
</section>
</div>
</div>
)
}
export default TokenManager

Best Practices and Tips

1. Error Handling

Always implement proper error handling:

const { data, error, isLoading } = useReadContract({
// ... configuration
query: {
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
staleTime: 1000 * 60 * 5, // 5 minutes
}
})
if (error) {
// Log error for debugging
console.error('Contract error:', error)

// Show user-friendly message
return <div>Failed to load data. Please try again.</div>
}

2. Loading States

Provide clear feedback to users:

function Component() {
const { data, isLoading, isFetching } = useReadContract({...})
  if (isLoading) return <Spinner />
if (isFetching) return <div className="updating">Updating... {data}</div>

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

3. Type Safety

Leverage TypeScript for better development experience:

// Define your contract types
type TokenContract = {
balanceOf: (account: Address) => Promise<bigint>
transfer: (to: Address, amount: bigint) => Promise<boolean>
}
// Use proper typing with hooks
const { data } = useReadContract({
address: '0x...',
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [address!],
}) as { data: bigint | undefined }

4. Performance Optimization

Optimize your app with proper configuration:

// Configure query client for optimal performance
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (was cacheTime)
refetchOnWindowFocus: false,
retry: 3,
},
},
})

5. Chain-Specific Logic

Handle multiple chains properly:

import { useChainId, useSwitchChain } from 'wagmi'
function ChainManager() {
const chainId = useChainId()
const { switchChain } = useSwitchChain()
const handleChainSwitch = (targetChainId: number) => {
if (chainId !== targetChainId) {
switchChain({ chainId: targetChainId })
}
}
return (
<div>
<p>Current Chain: {chainId}</p>
<button onClick={() => handleChainSwitch(1)}>
Switch to Ethereum
</button>
<button onClick={() => handleChainSwitch(137)}>
Switch to Polygon
</button>
</div>
)
}

Common Use Cases

1. DeFi Applications

  • Token Swaps: DEX integration with price quotes
  • Lending/Borrowing: Compound, Aave protocol interactions
  • Yield Farming: Staking rewards and LP token management
  • Liquidity Provision: AMM pool interactions

2. NFT Applications

  • Minting: Create and deploy NFT collections
  • Trading: Marketplace buy/sell functionality
  • Gallery: Display owned NFTs with metadata
  • Royalty Management: Creator fee distribution

3. Gaming DApps

  • In-game Assets: NFT-based items and characters
  • Reward Systems: Token-based achievements
  • Player vs Player: Stake-to-play mechanics
  • Guild Management: DAO governance for gaming guilds

4. DAO Applications

  • Governance: Proposal creation and voting
  • Treasury: Multi-sig wallet management
  • Token Distribution: Airdrop and vesting schedules
  • Member Management: Role-based access control

Migration from Wagmi v1

If you’re migrating from Wagmi v1, here are the key changes:

Configuration Updates

// v1
import { configureChains, createClient } from 'wagmi'
import { publicProvider } from 'wagmi/providers/public'
// v2
import { createConfig, http } from 'wagmi'

Hook Changes

// v1
import { useContractRead, useContractWrite } from 'wagmi'
// v2
import { useReadContract, useWriteContract } from 'wagmi'

Provider Updates

// v1
import { WagmiConfig } from 'wagmi'
// v2
import { WagmiProvider } from 'wagmi'

Key Breaking Changes

  1. New Config System: Use createConfig instead of createClient
  2. Updated Imports: Many hooks renamed for clarity
  3. Viem Integration: Replace ethers.js with viem
  4. TanStack Query: Required dependency for caching

Testing Wagmi Applications

Unit Testing with Jest and React Testing Library

import { render, screen, waitFor } from '@testing-library/react'
import { WagmiProvider, createConfig } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { mainnet } from 'wagmi/chains'
import { http } from 'viem'
import { TokenBalance } from './TokenBalance'
// Mock config for testing
const config = createConfig({
chains: [mainnet],
transports: {
[mainnet.id]: http(),
},
})
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
function TestWrapper({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
)
}
describe('TokenBalance', () => {
it('displays loading state initially', () => {
render(
<TestWrapper>
<TokenBalance
tokenAddress="0x..."
userAddress="0x..."
/>
</TestWrapper>
)

expect(screen.getByText(/loading/i)).toBeInTheDocument()
})
it('displays balance when data is loaded', async () => {
render(
<TestWrapper>
<TokenBalance
tokenAddress="0x..."
userAddress="0x..."
/>
</TestWrapper>
)

await waitFor(() => {
expect(screen.getByText(/balance:/i)).toBeInTheDocument()
})
})
})

Integration Testing

import { test, expect } from '@playwright/test'
test('wallet connection flow', async ({ page }) => {
await page.goto('http://localhost:3000')

// Click connect wallet
await page.click('text=Connect Wallet')

// Select MetaMask (in test environment)
await page.click('text=MetaMask')

// Verify connection
await expect(page.locator('text=Connected')).toBeVisible()
})

Security Best Practices

1. Input Validation

Always validate user inputs before sending transactions:

function SafeTransfer({ tokenAddress }: { tokenAddress: string }) {
const [recipient, setRecipient] = useState('')
const [amount, setAmount] = useState('')
  const validateInputs = () => {
// Validate recipient address
if (!recipient || !/^0x[a-fA-F0-9]{40}$/.test(recipient)) {
throw new Error('Invalid recipient address')
}
// Validate amount
const numAmount = parseFloat(amount)
if (isNaN(numAmount) || numAmount <= 0) {
throw new Error('Invalid amount')
}
return true
}
const { writeContract } = useWriteContract() const handleTransfer = () => {
try {
validateInputs()

writeContract({
address: tokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: 'transfer',
args: [recipient as `0x${string}`, parseEther(amount)],
})
} catch (error) {
alert(error.message)
}
}
return (
<div>
<input
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="Recipient address (0x...)"
/>
<input
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
type="number"
min="0"
step="0.01"
/>
<button onClick={handleTransfer}>Transfer</button>
</div>
)
}

2. Gas Estimation

Estimate gas before transactions:

import { useEstimateGas } from 'wagmi'
function GasEstimatedTransfer() {
const [recipient, setRecipient] = useState('')
const [amount, setAmount] = useState('')
const { data: gasEstimate } = useEstimateGas({
to: recipient as `0x${string}`,
value: amount ? parseEther(amount) : undefined,
query: {
enabled: !!(recipient && amount),
},
})
return (
<div>
<input
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="Recipient"
/>
<input
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>

{gasEstimate && (
<p>Estimated Gas: {gasEstimate.toString()} units</p>
)}

<button disabled={!gasEstimate}>
Send Transaction
</button>
</div>
)
}

3. Transaction Simulation

Use useSimulateContract to prevent failed transactions:

function SimulatedContractWrite() {
const [args, setArgs] = useState(['', ''])
  const { data: simulation, error: simulationError } = useSimulateContract({
address: '0x...',
abi: CONTRACT_ABI,
functionName: 'someFunction',
args: args,
query: {
enabled: args.every(arg => !!arg),
},
})
const { writeContract } = useWriteContract() const handleWrite = () => {
if (simulation?.request) {
writeContract(simulation.request)
}
}
return (
<div>
{/* Input fields */}

{simulationError && (
<div className="error">
⚠️ Transaction will fail: {simulationError.message}
</div>
)}

<button
onClick={handleWrite}
disabled={!simulation || !!simulationError}
>
Execute Transaction
</button>
</div>
)
}

Performance Optimization Tips

1. Selective Querying

Only fetch data when needed:

function ConditionalDataFetch({ shouldFetch }: { shouldFetch: boolean }) {
const { data } = useReadContract({
address: '0x...',
abi: CONTRACT_ABI,
functionName: 'getData',
query: {
enabled: shouldFetch, // Only fetch when needed
staleTime: 1000 * 60 * 5, // Cache for 5 minutes
},
})
  return <div>{shouldFetch && data}</div>
}

2. Batch Requests

Use useReadContracts for multiple related reads:

function OptimizedMultiRead() {
// Single request for multiple contract calls
const { data } = useReadContracts({
contracts: [
{ address: '0x...', abi: ABI, functionName: 'function1' },
{ address: '0x...', abi: ABI, functionName: 'function2' },
{ address: '0x...', abi: ABI, functionName: 'function3' },
],
})
  // Process results
const [result1, result2, result3] = data || []
return (
<div>
<p>Result 1: {result1?.result}</p>
<p>Result 2: {result2?.result}</p>
<p>Result 3: {result3?.result}</p>
</div>
)
}

3. Efficient Event Watching

Optimize event listening:

function OptimizedEventListener() {
const [events, setEvents] = useState([])
  useWatchContractEvent({
address: '0x...',
abi: CONTRACT_ABI,
eventName: 'Transfer',
args: {
from: '0x...', // Filter by specific address
},
onLogs: (logs) => {
// Process only relevant logs
const relevantLogs = logs.filter(log =>
log.args.value > parseEther('100')
)
setEvents(prev => [...relevantLogs, ...prev].slice(0, 50))
},
poll: true,
pollingInterval: 4000, // Poll every 4 seconds
})
return <EventList events={events} />
}

Debugging and Development Tools

1. Wagmi CLI

Use Wagmi CLI to generate type-safe contract hooks:

# Install Wagmi CLI
npm install -g @wagmi/cli
# Generate contract hooks
wagmi generate
# Watch for changes
wagmi generate --watch

2. Development Configuration

Set up helpful development tools:

const config = createConfig({
chains: [mainnet, sepolia],
transports: {
[mainnet.id]: http(
process.env.NODE_ENV === 'development'
? 'http://localhost:8545' // Local node
: 'https://eth-mainnet.alchemyapi.io/v2/...'
),
},
})
// Enable dev tools in development
if (process.env.NODE_ENV === 'development') {
import('@tanstack/react-query-devtools').then(({ ReactQueryDevtools }) => {
// Add React Query devtools
})
}

3. Error Logging

Implement comprehensive error logging:

function ErrorBoundary({ children }: { children: React.ReactNode }) {
const [hasError, setHasError] = useState(false)
  useEffect(() => {
const handleError = (error: any) => {
console.error('Wagmi Error:', error)
// Send to error reporting service
// errorReportingService.captureException(error)
setHasError(true)
}
window.addEventListener('error', handleError)
return () => window.removeEventListener('error', handleError)
}, [])
if (hasError) {
return <div>Something went wrong with the blockchain connection.</div>
}
return <>{children}</>
}

Deployment Considerations

1. Environment Configuration

Set up proper environment variables:

// config/wagmi.ts
const getConfig = () => {
const isProduction = process.env.NODE_ENV === 'production'

return createConfig({
chains: isProduction ? [mainnet] : [mainnet, sepolia],
transports: {
[mainnet.id]: http(process.env.NEXT_PUBLIC_MAINNET_RPC_URL),
...(process.env.NODE_ENV !== 'production' && {
[sepolia.id]: http(process.env.NEXT_PUBLIC_SEPOLIA_RPC_URL),
}),
},
connectors: [
injected(),
walletConnect({
projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID!
})
],
})
}

2. Bundle Optimization

Optimize your bundle size:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.resolve.fallback = {
fs: false,
net: false,
tls: false,
}
return config
},
experimental: {
optimizePackageImports: ['wagmi', 'viem']
}
}
module.exports = nextConfig

Conclusion

Wagmi v2 represents a significant evolution in Web3 development tooling. It provides:

Get mirbasit01’s stories in your inbox

Join Medium for free to get updates from this writer.

Key Benefits:

  • Type Safety: Full TypeScript support reduces runtime errors
  • Developer Experience: Intuitive React hooks API
  • Performance: Optimized caching and request batching
  • Flexibility: Support for multiple chains and wallets
  • Community: Active ecosystem and excellent documentation

When to Use Wagmi v2:

  • Building React-based Web3 applications
  • Need for type-safe blockchain interactions
  • Require multi-chain support
  • Want optimized performance and caching
  • Developing production-grade DApps

Getting Started Checklist:

  1. ✅ Install Wagmi v2 and dependencies
  2. ✅ Configure chains and providers
  3. ✅ Set up wallet connectors
  4. ✅ Implement basic read/write operations
  5. ✅ Add error handling and loading states
  6. ✅ Optimize for production deployment

Wagmi v2 makes building Web3 applications more accessible and maintainable than ever before. Whether you’re creating a simple token transfer app or a complex DeFi protocol, Wagmi provides the tools you need to build robust, user-friendly decentralized applications.

Start exploring Wagmi v2 today and experience the future of Web3 development!

--

--

mirbasit01

Written by mirbasit01

Front-End Developer | React.js | Next.js | TypeScript | Tailwind CSS | Web3.js | Smart Contracts | Creating responsive & innovative web apps