Imagine your React app uses ethers.js to interact with contracts. When the user switches networks, the data doesn't update, and N+1 requests to the RPC choke the application. We've encountered this many times — hydration mismatches in Next.js, memory leaks from ethers.js mutable state. The solution is the wagmi + viem combo. Our Web3 development experience spans over 5 years, and we've implemented this solution in 10+ DeFi projects. Switching to wagmi/viem reduces code volume by 2–3x and decreases TTFB by 40%, and lowers RPC request costs thanks to automatic caching.
Why ethers.js Slows Things Down
ethers.js is a class-oriented library with mutable state. Each new Contract(...) creates an object tied to a provider. In React, this is problematic: when the network or account changes, the entire object must be recreated, otherwise data doesn't update. Viem is built on functions and immutable clients. Wagmi automatically manages the client lifecycle — hooks update on any wallet changes. According to our stats, switching to wagmi/viem reduces code volume by 2–3x and decreases TTFB by 40%.
Comparison: ethers.js vs wagmi/viem
| Criteria | ethers.js | wagmi + viem |
|---|---|---|
| State | Mutable, objects live in context | Immutable, no recreation needed |
| React integration | Manual via useState/useEffect | Built-in hooks with caching |
| Update on network change | Need to subscribe to events | Automatic refetch via watch |
| Typing | Weak, manual casts | Full typing via @wagmi/cli |
| Performance | Higher latency due to class | Lower TTFB due to lazy calls |
How wagmi/viem Solve ethers.js Problems
Wagmi hooks automatically subscribe to wallet events (chainChanged, accountsChanged). This eliminates stale data issues. Viem clients are immutable — each call returns a fresh result. Together they provide reactivity without memory leaks. For example, useReadContract automatically refetches data on network change, and useWatchContractEvent filters events without creating new subscriptions on re-render. We use this approach in production projects, reducing RPC load and improving user experience. As noted in the official viem documentation, "viem is designed to be modular, tree-shakeable, and type-safe out of the box".
What ABI Type Generation Brings
Without type generation, each contract call requires manually specifying the ABI and casting results. Wagmi CLI generates types from your ABI files in one command. After generation, you get hooks with full typing of arguments and return values. This eliminates errors in addresses, arguments, and allows the editor to suggest possible values. Simply put, you import useReadErc20BalanceOf and pass the required address — everything is checked at compile time.
npm i -D @wagmi/cli
npx wagmi generate
How We Implement wagmi/viem: Step by Step
Configuration Setup
// 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!),
},
});
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 with Simulation
// 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;
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,
};
}
Server-Side viem Client
// 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!),
});
export const serverWallet = createWalletClient({
account: privateKeyToAccount(process.env.RELAYER_PRIVATE_KEY as `0x${string}`),
chain: mainnet,
transport: http(process.env.ETH_RPC_URL!),
});
Type Generation from ABI
npm i -D @wagmi/cli
npx wagmi generate
After generation, hooks like useReadErc20BalanceOf(...) appear with full argument typing.
Example Usage in Next.js App Router
In server components, use viem directly since wagmi works only on the client. Create a publicClient and use it for data reads. For instance, in serverComponent.tsx, import publicClient from lib/publicClient.ts and call methods without React hooks.
Our Work Process
- Audit — Analyze current implementation, identify issues (memory leaks, data staleness).
- Design — Design the interaction layer: configs, hooks, server clients.
- Implementation — Write code with type generation, cover edge cases.
- Testing — Simulations, network tests, manual checks with various wallets.
- Deployment — Deploy, monitor, document.
What's Included
- Integration code with wagmi/viem: typed hooks.
- Documentation for use and further development.
- CI/CD setup for automatic ABI updates.
- Team training (1–2 sessions).
- Compatibility guarantee with MetaMask, WalletConnect, Coinbase Wallet.
Timelines
Basic integration (read/write for 1–2 contracts) — 1–2 days. Full layer with type generation, server clients, and event subscriptions — 2–3 days. Timelines are refined after auditing your project.
Order an Audit of Your Integration — We'll Find Bottlenecks in 1 Day
Approach Comparison: ethers.js vs wagmi/viem
| Parameter | ethers.js | wagmi/viem |
|---|---|---|
| Boilerplate | High (manual provider management) | Low (automatic configuration) |
| Data update | Manual via useEffect | Automatic via watch |
| Type safety | No | Full (via @wagmi/cli) |
| Performance | ~2 ms per call | <1 ms per call (lazy evaluation) |
| Server-side support | Complex (needs ethers.providers) | Simple (viem createPublicClient) |
Contact us — we'll discuss your project details and choose the optimal architecture.







