Viem Frontend Integration
ethers.js v5 weighed 285KB gzipped, had mutable providers, and wasn't written with tree-shaking in mind. viem is a rethinking of how a TypeScript client for EVM should look in 2024: modular architecture, zero-dep core, full TypeScript with type inference from ABI.
Key Concepts of Viem
Clients: Public, Wallet, Test
In viem there's no single "provider". There are three types of clients with different capabilities:
import { createPublicClient, createWalletClient, http, custom } from "viem"
import { mainnet } from "viem/chains"
// For reading data from blockchain (wallet not needed)
const publicClient = createPublicClient({
chain: mainnet,
transport: http("https://eth-mainnet.g.alchemy.com/v2/KEY")
})
// For signing and sending transactions (wallet needed)
const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum)
})
transport can be http(), webSocket(), fallback([http(...), http(...)]) — automatic failover between RPC providers. This alone solves reliability problems that used to be solved manually.
ABI Typing — Main Advantage
Viem generates types from ABI at compile time. If you pass a wrong argument — TypeScript error, not runtime revert:
const abi = [
{
name: "transfer",
type: "function",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" }
],
outputs: [{ type: "bool" }]
}
] as const // important: as const for type inference
// TypeScript knows: first argument is `0x${string}`, second is bigint
const hash = await walletClient.writeContract({
address: "0xTokenAddress",
abi,
functionName: "transfer",
args: ["0xRecipient", parseEther("1.0")]
})
Without as const — all typing is lost. This is an important detail when working with viem.
Encoding/Decoding Data
import { encodeAbiParameters, decodeAbiParameters, parseAbi } from "viem"
// Decode transaction data
const decoded = decodeAbiParameters(
parseAbi(["function transfer(address to, uint256 amount)"]),
txData
)
// Encode calldata manually
const calldata = encodeAbiParameters(
[{ type: "address" }, { type: "uint256" }],
["0xRecipient", parseEther("1.0")]
)
parseUnits / formatUnits / parseEther / formatEther — utilities for working with BigInt values. All numbers in viem are native JavaScript BigInt, not ethers.BigNumber.
Integration with React via Wagmi
viem is a transport layer. For React applications — wagmi (hooks over viem):
import { useReadContract, useWriteContract, useAccount } from "wagmi"
function TokenBalance({ tokenAddress }: { tokenAddress: `0x${string}` }) {
const { address } = useAccount()
const { data: balance } = useReadContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [address!],
query: { enabled: !!address }
})
const { writeContract, isPending } = useWriteContract()
const transfer = (to: `0x${string}`, amount: bigint) =>
writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "transfer",
args: [to, amount]
})
return <div>Balance: {balance ? formatEther(balance) : "..."}</div>
}
wagmi v2 is fully built on viem, caches results via TanStack Query. useReadContract is essentially a useQuery wrapper over publicClient.readContract.
Multichain Configuration
import { createConfig, http } from "wagmi"
import { mainnet, polygon, arbitrum, base } from "wagmi/chains"
export const config = createConfig({
chains: [mainnet, polygon, arbitrum, base],
transports: {
[mainnet.id]: http("https://eth-mainnet.g.alchemy.com/v2/KEY"),
[polygon.id]: http("https://polygon-mainnet.g.alchemy.com/v2/KEY"),
[arbitrum.id]: http("https://arb-mainnet.g.alchemy.com/v2/KEY"),
[base.id]: http("https://base-mainnet.g.alchemy.com/v2/KEY"),
}
})
Custom networks (testnets, L2 without support in viem/chains) are defined manually:
import { defineChain } from "viem"
const customChain = defineChain({
id: 1234,
name: "Custom Network",
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.custom.network"] } }
})
Migration from ethers.js v5
| ethers.js v5 | viem |
|---|---|
new ethers.providers.Web3Provider(window.ethereum) |
createWalletClient({ transport: custom(window.ethereum) }) |
provider.getBalance(address) |
publicClient.getBalance({ address }) |
contract.balanceOf(address) |
publicClient.readContract({ abi, functionName: 'balanceOf', args: [address] }) |
ethers.utils.parseEther("1.0") |
parseEther("1.0") |
ethers.BigNumber.from("100") |
BigInt("100") |
signer.signMessage(message) |
walletClient.signMessage({ message }) |
The main mindset change: instead of contract objects (new ethers.Contract(...)) — functions with explicit abi and address. Verbose, but type-safe.
Timeline
Setting up viem + wagmi with multichain configuration, wallet connection and basic read/write operations — 1-2 days. Migrating existing project with ethers.js — depends on volume, usually 2-5 days.







