Implementing Blockchain Interaction via wagmi/viem on a Website
wagmi + viem is the modern standard for Web3 frontend on React. wagmi provides hooks (useAccount, useReadContract, useWriteContract), viem is a low-level typed client. This combination replaces ethers.js + react-query: less boilerplate, built-in caching, automatic refetch by blocks.
How It Differs From ethers.js
ethers.js is a class-oriented library with mutable state. new Contract(...) creates an object that holds the provider inside itself. In React this is a problem: when the network or account changes, the object needs to be recreated.
viem is built on functions and immutable clients. wagmi manages the lifecycle of clients automatically — when the account changes, hooks automatically update data.
// ethers.js approach
const provider = new BrowserProvider(window.ethereum);
const contract = new Contract(address, abi, provider);
const balance = await contract.balanceOf(walletAddress);
// viem/wagmi approach
const balance = await readContract(publicClient, {
address,
abi,
functionName: 'balanceOf',
args: [walletAddress],
});
Configuration
// lib/wagmi.ts
import { createConfig, http } from 'wagmi';
import { mainnet, arbitrum, base } from 'wagmi/chains';
import { injected, walletConnect } from 'wagmi/connectors';
export const config = createConfig({
chains: [mainnet, arbitrum, base],
connectors: [
injected(),
walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! }),
],
transports: {
[mainnet.id]: http(process.env.ETH_RPC_URL!),
[arbitrum.id]: http(process.env.ARBITRUM_RPC_URL!),
[base.id]: http(process.env.BASE_RPC_URL!),
},
});
// app/providers.tsx
'use client';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from '@/lib/wagmi';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
);
}
Reading Data from a Contract
// hooks/useTokenData.ts
import { useReadContracts } from 'wagmi';
import { erc20Abi, formatUnits } from 'viem';
export function useTokenData(tokenAddress: `0x${string}`, userAddress?: `0x${string}`) {
const { data, isLoading } = useReadContracts({
contracts: [
{ address: tokenAddress, abi: erc20Abi, functionName: 'name' },
{ address: tokenAddress, abi: erc20Abi, functionName: 'symbol' },
{ address: tokenAddress, abi: erc20Abi, functionName: 'decimals' },
{ address: tokenAddress, abi: erc20Abi, functionName: 'totalSupply' },
...(userAddress ? [{
address: tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf' as const,
args: [userAddress] as [`0x${string}`],
}] : []),
],
query: {
refetchInterval: 30_000,
staleTime: 10_000,
},
});
const decimals = (data?.[2].result as number) ?? 18;
return {
isLoading,
name: data?.[0].result as string | undefined,
symbol: data?.[1].result as string | undefined,
decimals,
totalSupply: data?.[3].result
? formatUnits(data[3].result as bigint, decimals)
: undefined,
userBalance: userAddress && data?.[4]?.result
? formatUnits(data[4].result as bigint, decimals)
: undefined,
};
}
Writing to a Contract
// hooks/useTokenTransfer.ts
import { useWriteContract, useWaitForTransactionReceipt, useSimulateContract } from 'wagmi';
import { erc20Abi, parseUnits } from 'viem';
import { useState } from 'react';
export function useTokenTransfer(tokenAddress: `0x${string}`, decimals: number) {
const [recipient, setRecipient] = useState<`0x${string}` | undefined>();
const [amount, setAmount] = useState('');
const amountWei = amount ? parseUnits(amount, decimals) : 0n;
// Simulate before signing — catch errors without gas
const { error: simError } = useSimulateContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'transfer',
args: [recipient!, amountWei],
query: { enabled: !!recipient && amountWei > 0n },
});
const { writeContract, data: txHash, isPending } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash: txHash });
const transfer = () => {
if (!recipient || amountWei === 0n) return;
writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'transfer',
args: [recipient, amountWei],
});
};
return {
recipient, setRecipient,
amount, setAmount,
simError,
transfer,
txHash,
isPending,
isConfirming,
isSuccess,
};
}
Direct viem Client for Server Usage
wagmi works only in React context. For API routes, server components, cron — use viem directly:
// lib/publicClient.ts
import { createPublicClient, http, createWalletClient } from 'viem';
import { mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
export const publicClient = createPublicClient({
chain: mainnet,
transport: http(process.env.ETH_RPC_URL!),
});
// Server wallet client (for automatic transactions, relayer)
export const serverWallet = createWalletClient({
account: privateKeyToAccount(process.env.RELAYER_PRIVATE_KEY as `0x${string}`),
chain: mainnet,
transport: http(process.env.ETH_RPC_URL!),
});
Subscribing to Events via viem
// Polling for HTTP transport
import { watchContractEvent } from 'viem/actions';
const unwatch = watchContractEvent(publicClient, {
address: contractAddress,
abi: contractAbi,
eventName: 'Transfer',
onLogs: (logs) => {
for (const log of logs) {
console.log(log.args);
}
},
poll: true,
pollingInterval: 4_000,
});
// For WebSocket transport — push, not polling
import { webSocket } from 'viem';
const wsClient = createPublicClient({
chain: mainnet,
transport: webSocket(process.env.ETH_WS_URL!),
});
Type Generation from ABI
# Install wagmi CLI
npm i -D @wagmi/cli
# Auto-generate typed hooks
npx wagmi generate
After generation, hooks like useReadErc20BalanceOf(...) appear with full type safety of arguments — passing an incorrect type will be caught at compile time.
Timeframe: setting up wagmi/viem in a project, connecting a wallet, reading and writing to 1–2 contracts — 1–2 days. Full interaction layer with type generation, server clients and event subscription — 2–3 days.







