TL;DR
Web3 JavaScript tooling has undergone a significant generational shift. ethers.js v6 is the long-standing standard — a complete Ethereum library for interacting with contracts, wallets, and providers; battle-tested with millions of users but showing its age in tree-shaking and TypeScript ergonomics. viem is the modern replacement — a TypeScript-first, tree-shakeable, low-level Ethereum library that's lighter, faster, and more type-safe than ethers.js v6; it's what wagmi v2 is built on. wagmi v2 is the React layer on top of viem — hooks for wallet connection, contract reads/writes, account state, and transaction status with React Query caching built-in. For low-level Ethereum interaction in Node.js or scripts: viem. For React dApps with wallet connections: wagmi. For projects with existing ethers.js integrations: ethers.js v6 or migrate to viem.
Key Takeaways
- wagmi v2 is built on viem — they're the same ecosystem, complementary not competing
- viem has strict TypeScript types — contract ABI types flow through to call results
- ethers.js v6 bundle: ~200kB — viem is ~35kB for the same functionality
- wagmi handles wallet connection — MetaMask, WalletConnect, Coinbase Wallet via connectors
- viem uses Actions pattern —
publicClient.readContract()vs ethers.jscontract.method() - wagmi useContractRead is cached — backed by React Query, auto-refetch on block updates
- All three support EVM chains — Ethereum, Polygon, Arbitrum, Base, Optimism, etc.
Ecosystem Relationships
ethers.js v6 → monolithic library, standalone
viem → modular, TypeScript-first, low-level
wagmi v2 → React hooks, built on viem
useAccount, useBalance, useContractRead, useWriteContract
Usage patterns:
React dApp with wallets → wagmi (useAccount, useContractRead)
Node.js script / backend → viem (createPublicClient, createWalletClient)
Non-React frontend → viem directly
Legacy project on ethers v5 → migrate to viem or ethers v6
viem: Modern Ethereum Library
viem is a TypeScript-first, tree-shakeable Ethereum library designed as the modern alternative to ethers.js. It uses a client-action architecture.
Installation
npm install viem
Create Clients
import { createPublicClient, createWalletClient, http, custom } from "viem";
import { mainnet, polygon, base, arbitrum } from "viem/chains";
// Public client — read operations (no wallet needed)
export const publicClient = createPublicClient({
chain: mainnet,
transport: http(process.env.ALCHEMY_MAINNET_URL!), // Or infura, quicknode
});
// Wallet client — write operations (wallet required)
export const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum!), // MetaMask or any EIP-1193 provider
});
// Multiple chains
export const polygonClient = createPublicClient({
chain: polygon,
transport: http(process.env.ALCHEMY_POLYGON_URL!),
});
Read Contract
import { parseAbi } from "viem";
// Type-safe ABI — return types are inferred from ABI
const ERC20_ABI = parseAbi([
"function balanceOf(address owner) view returns (uint256)",
"function totalSupply() view returns (uint256)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function transfer(address to, uint256 amount) returns (bool)",
]);
async function getTokenBalance(tokenAddress: `0x${string}`, ownerAddress: `0x${string}`) {
const balance = await publicClient.readContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: "balanceOf",
args: [ownerAddress],
});
// balance is typed as bigint (inferred from ABI return type)
return balance;
}
// Read multiple values in one call (multicall)
const [symbol, decimals, totalSupply] = await publicClient.multicall({
contracts: [
{ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", abi: ERC20_ABI, functionName: "symbol" },
{ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", abi: ERC20_ABI, functionName: "decimals" },
{ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", abi: ERC20_ABI, functionName: "totalSupply" },
],
});
Write Contract (Send Transaction)
import { parseEther, parseUnits } from "viem";
async function transferTokens(
tokenAddress: `0x${string}`,
toAddress: `0x${string}`,
amount: bigint
): Promise<`0x${string}`> {
const [account] = await walletClient.getAddresses();
// Simulate first (checks for errors without sending)
const { request } = await publicClient.simulateContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: "transfer",
args: [toAddress, amount],
account,
});
// Send transaction
const hash = await walletClient.writeContract(request);
// Wait for confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash });
return hash;
}
// Send ETH
async function sendEth(toAddress: `0x${string}`, amountEth: string) {
const [account] = await walletClient.getAddresses();
const hash = await walletClient.sendTransaction({
account,
to: toAddress,
value: parseEther(amountEth), // "0.1" → 100000000000000000n
});
return hash;
}
Listen to Events
// Watch for Transfer events
const unwatch = publicClient.watchContractEvent({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
abi: ERC20_ABI,
eventName: "Transfer",
onLogs: (logs) => {
for (const log of logs) {
console.log(`Transfer: ${log.args.from} → ${log.args.to}: ${log.args.value}`);
}
},
});
// Stop watching
unwatch();
wagmi v2: React Hooks for Web3
wagmi v2 provides React hooks built on viem and React Query — wallet connection, contract reads/writes, and account state.
Installation
npm install wagmi viem @tanstack/react-query
Provider Setup
// lib/wagmi.ts
import { createConfig, http } from "wagmi";
import { mainnet, polygon, base } from "wagmi/chains";
import { injected, metaMask, walletConnect, coinbaseWallet } from "wagmi/connectors";
export const config = createConfig({
chains: [mainnet, polygon, base],
connectors: [
injected(), // MetaMask and other browser wallets
metaMask(), // MetaMask specifically
walletConnect({ projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID! }),
coinbaseWallet({ appName: "My App" }),
],
transports: {
[mainnet.id]: http(process.env.NEXT_PUBLIC_ALCHEMY_MAINNET_URL!),
[polygon.id]: http(process.env.NEXT_PUBLIC_ALCHEMY_POLYGON_URL!),
[base.id]: http(),
},
});
// app/providers.tsx
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>
);
}
Connect Wallet
import { useConnect, useDisconnect, useAccount } from "wagmi";
function ConnectWallet() {
const { connectors, connect, isPending } = useConnect();
const { disconnect } = useDisconnect();
const { address, isConnected, chain } = useAccount();
if (isConnected) {
return (
<div>
<p>Connected: {address?.slice(0, 6)}...{address?.slice(-4)}</p>
<p>Chain: {chain?.name}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
}
return (
<div>
{connectors.map((connector) => (
<button
key={connector.id}
onClick={() => connect({ connector })}
disabled={isPending}
>
{connector.name}
</button>
))}
</div>
);
}
Read Contract (useReadContract)
import { useReadContract, useReadContracts } from "wagmi";
import { formatUnits } from "viem";
function TokenBalance({ tokenAddress, ownerAddress }: { tokenAddress: `0x${string}`; ownerAddress: `0x${string}` }) {
const { data: balance, isLoading } = useReadContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: "balanceOf",
args: [ownerAddress],
});
const { data: decimals } = useReadContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: "decimals",
});
if (isLoading) return <span>Loading...</span>;
const formatted = balance && decimals ? formatUnits(balance, decimals) : "0";
return <span>{formatted}</span>;
}
// Read multiple contracts in parallel
function TokenInfo({ tokenAddress }: { tokenAddress: `0x${string}` }) {
const { data } = useReadContracts({
contracts: [
{ address: tokenAddress, abi: ERC20_ABI, functionName: "symbol" },
{ address: tokenAddress, abi: ERC20_ABI, functionName: "decimals" },
{ address: tokenAddress, abi: ERC20_ABI, functionName: "totalSupply" },
],
});
if (!data) return null;
const [symbol, decimals, totalSupply] = data;
return (
<div>
<p>Symbol: {symbol.result}</p>
<p>Decimals: {decimals.result}</p>
<p>Total Supply: {totalSupply.result?.toString()}</p>
</div>
);
}
Write Contract (useWriteContract)
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
function TransferButton({ tokenAddress, toAddress, amount }: {
tokenAddress: `0x${string}`;
toAddress: `0x${string}`;
amount: bigint;
}) {
const { writeContract, data: hash, isPending } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash,
});
function handleTransfer() {
writeContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: "transfer",
args: [toAddress, amount],
});
}
return (
<div>
<button onClick={handleTransfer} disabled={isPending || isConfirming}>
{isPending ? "Waiting for wallet..." : isConfirming ? "Confirming..." : "Transfer"}
</button>
{isSuccess && <p>✅ Transfer confirmed! Hash: {hash}</p>}
</div>
);
}
ethers.js v6: The Established Standard
ethers.js v6 remains widely used — it's the library most Ethereum tutorials, documentation, and Stack Overflow answers reference.
Installation
npm install ethers
Provider and Signer
import { ethers, BrowserProvider, JsonRpcProvider, Contract } from "ethers";
// Node.js: JSON-RPC provider
const provider = new JsonRpcProvider(process.env.ALCHEMY_URL!);
// Browser: wallet provider (MetaMask)
async function getBrowserSigner() {
const provider = new BrowserProvider(window.ethereum!);
const signer = await provider.getSigner();
return { provider, signer };
}
Read Contract
async function getBalance(tokenAddress: string, ownerAddress: string) {
const provider = new JsonRpcProvider(process.env.ALCHEMY_URL!);
const contract = new Contract(tokenAddress, ERC20_ABI_ARRAY, provider);
// Returns BigInt in ethers v6
const balance: bigint = await contract.balanceOf(ownerAddress);
const decimals: bigint = await contract.decimals();
return ethers.formatUnits(balance, decimals); // "1234.56"
}
Write Contract
async function transferTokens(tokenAddress: string, toAddress: string, amount: string) {
const provider = new BrowserProvider(window.ethereum!);
const signer = await provider.getSigner();
const contract = new Contract(tokenAddress, ERC20_ABI_ARRAY, signer);
const amountWei = ethers.parseUnits(amount, 18); // "10" → 10000000000000000000n
const tx = await contract.transfer(toAddress, amountWei);
console.log("Transaction hash:", tx.hash);
const receipt = await tx.wait();
console.log("Confirmed in block:", receipt?.blockNumber);
}
Feature Comparison
| Feature | wagmi v2 | viem | ethers.js v6 |
|---|---|---|---|
| Use case | React dApps | Low-level / Node.js | Any JS environment |
| React hooks | ✅ | ❌ | ❌ |
| Built on | viem | — | own engine |
| Bundle size | ~65kB | ~35kB | ~200kB |
| TypeScript types | ✅ ABI-inferred | ✅ ABI-inferred | ✅ Good |
| Tree-shakeable | ✅ | ✅ | ✅ v6 improved |
| Multicall | ✅ | ✅ | Via plugin |
| React Query cache | ✅ | ❌ | ❌ |
| Wallet connectors | ✅ 15+ | ❌ | ❌ |
| ENS support | ✅ | ✅ | ✅ |
| GitHub stars | 6.2k | 10k | 8k |
| npm weekly | 600k | 1.5M | 1.7M |
When to Use Each
Choose wagmi + viem if:
- Building a React dApp that needs wallet connection (MetaMask, WalletConnect, Coinbase)
- React Query caching for contract reads (auto-refetch every block, stale-while-revalidate)
- Full TypeScript inference from ABI — catch contract call errors at compile time
- Modern stack starting a new project in 2026
Choose viem alone if:
- Node.js scripts, bots, or backend services that interact with contracts
- Non-React frontends (Vue, Svelte, vanilla JS)
- You want the viem ecosystem without React hooks overhead
Choose ethers.js v6 if:
- Existing codebase already uses ethers.js and migration cost is high
- Following tutorials or documentation written for ethers.js
- Mature ecosystem with many middleware libraries built on ethers
- Simple scripts where bundle size doesn't matter
ABI-Driven Type Safety: The Real Advantage of viem
The most consequential technical difference between viem/wagmi and ethers.js is not performance or bundle size — it's how TypeScript types flow from contract ABIs to your application code.
In ethers.js, when you call contract.balanceOf(address), TypeScript knows the return type is Promise<any>. The ABI you pass to new Contract() is a runtime object, not a type-level construct. You manually annotate return types and hope they match reality:
const balance: bigint = await contract.balanceOf(address) // hope this is right
In viem, the ABI is a TypeScript constant and the type system derives the return type directly from it. When you call readContract({ abi: ERC20_ABI, functionName: "balanceOf" }), TypeScript infers that the return type is bigint — not from a manual annotation, but from reading the ABI definition at the type level. If the function doesn't exist in the ABI, TypeScript errors at compile time. If the args type doesn't match what the ABI expects, TypeScript errors. If you try to call a write function on a public client (read-only), TypeScript errors.
This ABI-driven type inference is viem's killer feature for teams building complex dApps with multiple contracts. Contract interactions become as type-safe as regular TypeScript function calls. Wagmi extends this to React hooks: useReadContract and useWriteContract infer return types from the ABI, so the data you receive from the hook is typed to the exact return value of the specific contract function you called.
Multi-Chain Support and Chain Switching
Modern dApps frequently operate across multiple EVM chains — Ethereum mainnet for security, Polygon or Base for low fees, Arbitrum for DeFi, and testnets for development. All three libraries support multi-chain development, but wagmi's implementation is the most ergonomic.
With wagmi, you declare supported chains in the config once and handle chain switching through the useSwitchChain hook. The useAccount hook gives you the currently connected chain, and you can configure per-chain transports in a single object. Contract reads automatically use the transport matching the current chain.
Viem requires creating a separate publicClient for each chain you want to interact with. For Node.js backends this is fine — you instantiate clients for each chain at startup. For React dApps, managing multiple client instances and selecting the right one based on the connected wallet's current chain adds boilerplate that wagmi abstracts away.
Ethers.js handles multi-chain through separate provider instances per chain, similar to viem. The lack of a React-specific layer means chain switching (responding to the user's wallet changing networks) requires manual event listeners on the provider object and React state updates — functionality that wagmi provides out of the box via the useChainId hook and automatic connector subscription.
Migrating from ethers.js to viem
For teams considering migrating from ethers.js v6 to viem, the core concepts map directly:
new JsonRpcProvider(url) becomes createPublicClient({ transport: http(url) }). new BrowserProvider(window.ethereum) becomes createWalletClient({ transport: custom(window.ethereum) }). new Contract(address, abi, signer) is replaced by the stateless readContract and writeContract actions. ethers.parseUnits("1.0", 18) becomes parseUnits("1.0", 18) from viem directly.
The most meaningful API change is the shift from object-oriented (contract instance with methods) to functional (client with actions). Viem has no Contract class — you pass the address, ABI, and function name to each call. For teams with many contract call sites, this is a significant refactor, but it produces more explicit, greppable code where each call clearly identifies what it's calling.
Methodology
Data sourced from official wagmi documentation (wagmi.sh), viem documentation (viem.sh), ethers.js v6 documentation (docs.ethers.org), npm weekly download statistics (February 2026), GitHub star counts as of February 2026, bundle size analysis from bundlephobia.com, and community discussions from the Ethereum StackExchange and r/ethdev.
Gas Estimation and Transaction Cost Management
Gas estimation is a practical concern that distinguishes production-grade dApp code from tutorial examples. All three libraries expose gas estimation — estimateGas in ethers.js v6, estimateContractGas in viem, and useEstimateGas in wagmi — but the details matter for user experience. Underestimating gas causes transaction failures after the user has already paid the network fee for the failed attempt. Overestimating inflates the perceived cost and reduces trust. The recommended pattern is to call estimateGas with the exact transaction parameters, apply a 10-20% buffer multiplier, and display the estimated cost to the user before they confirm. Viem's simulateContract function goes further: it dry-runs the contract call against the current chain state and returns both the expected return value and the gas estimate in a single call, which reduces RPC round-trips compared to calling readContract and estimateContractGas separately. For high-stakes transactions (large token swaps, NFT mints with variable gas), adding a maxFeePerGas cap derived from getFeeHistory prevents the transaction from executing at unexpectedly high gas prices during network congestion spikes.
Related: Vercel AI SDK vs OpenAI SDK vs Anthropic SDK for AI/LLM SDKs following a similar pattern of React hooks + low-level clients, or Shopify Hydrogen vs Medusa vs Commerce.js for commerce SDK patterns that parallel Web3 contract interaction.
See also: React vs Vue and React vs Svelte