Skip to main content

wagmi vs ethers.js vs viem: Web3 JavaScript Libraries 2026

·PkgPulse Team

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 patternpublicClient.readContract() vs ethers.js contract.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

Featurewagmi v2viemethers.js v6
Use caseReact dAppsLow-level / Node.jsAny JS environment
React hooks
Built onviemown engine
Bundle size~65kB~35kB~200kB
TypeScript types✅ ABI-inferred✅ ABI-inferred✅ Good
Tree-shakeable✅ v6 improved
MulticallVia plugin
React Query cache
Wallet connectors✅ 15+
ENS support
GitHub stars6.2k10k8k
npm weekly600k1.5M1.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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.