wagmi vs ethers.js vs viem: Web3 JavaScript Libraries 2026
wagmi vs ethers.js vs viem: Web3 JavaScript Libraries 2026
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
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.
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.