Web3 Development · 5 min read ·

Web3 Frontends with wagmi + viem: A Practical Guide

Learn how to build reliable Web3 frontends using wagmi and viem: wallet UX, reads/writes, chain config, contracts, and production best practices.

Web3 frontend development with wagmi and viem

Building Web3 frontends used to mean wrestling with provider quirks, brittle RPC calls, and wallet UX that felt bolted on. Today, the wagmi + viem stack is the most pragmatic way to ship production-grade Ethereum (and EVM) apps in React: wagmi gives you ergonomic React hooks and state management; viem gives you a modern, type-safe Ethereum client that avoids a lot of the historical footguns.

This article focuses on how to actually build with the stack—connect wallets cleanly, read/write contracts reliably, and avoid common pitfalls that show up once you have real users.

Why wagmi + viem is the default stack now

wagmi is a React toolkit for Ethereum. It offers:

  • Wallet connection flows (connectors, account state)
  • Query-cached contract reads (via TanStack Query)
  • Hooks for signing, transactions, and network switching

viem is the underlying TypeScript client used by wagmi v1+ (replacing ethers as the default). viem’s big wins:

  • Strong typing and ABI-first patterns (less runtime guessing)
  • Predictable serialization (bigints, hex, addresses)
  • A cleaner, more explicit API surface for reads/writes

Opinionated take: if you’re starting in 2026 and you’re still reaching for “raw ethers provider + custom hooks,” you’re opting into maintenance work you don’t need.

Project setup: provider, chains, and transports

A solid baseline is: configure chains once, set up transports (RPC endpoints), then wrap your app with WagmiProvider.

Key choices you’ll make early:

  • Which chains you support (mainnet, Base, Arbitrum, Polygon, etc.)
  • RPC strategy (free public RPC vs paid providers vs your own node)
  • Wallet support (injected, WalletConnect, Coinbase Wallet)

A typical setup includes:

  • @wagmi/core and wagmi for hooks
  • viem for utilities and types
  • @tanstack/react-query for caching

In practice, you should treat RPC selection as a production concern. Public RPCs are fine for prototyping, but they’ll throttle or degrade under load.

Wallet connection UX: make it boring

Most Web3 apps fail users before they ever sign anything.

With wagmi, you model connection state via hooks like useAccount, useConnect, and useDisconnect. The UX you want:

  • Show “Connect” when disconnected
  • Once connected, show the address (shortened), chain name, and a clear disconnect option
  • If the user is on the wrong chain, guide them to switch (don’t just throw errors)

Practical tips:

  • Don’t auto-pop wallets unexpectedly. Trigger wallet prompts only from explicit user actions.
  • Persist connection state carefully. Auto-reconnect is great until it causes confusing popups on page load.
  • Handle unsupported chains. If you support Base + Arbitrum, tell users exactly that.

If you’re building for mobile, you almost certainly want WalletConnect. For desktop, injected wallets (MetaMask, Rabby) cover a lot of ground.

Reading contract state: caching and multicalls

Reads are where wagmi shines because it integrates with query caching. Use useReadContract for single reads and useReadContracts for batches.

This matters because in real apps you often need multiple pieces of state:

  • User balance
  • Allowance to a spender
  • Pool reserves / price
  • Protocol configuration flags

Batching these reads (or using multicall under the hood) improves UX and reduces RPC load.

Best practices for reads:

  • Always define ABIs as const so TypeScript infers types.
  • Use enabled flags to avoid reads before prerequisites exist (e.g., no address yet).
  • Set sensible refetch behavior. Not everything needs polling; refetch on block only where necessary.

Example scenario: on a token swap screen, you might poll quotes every 10–15 seconds, but you shouldn’t poll static metadata constantly.

Writing transactions: simulate first, then send

Blindly calling writeContract is how you ship broken UX.

The production pattern is:

  1. Simulate the transaction (dry-run) to catch reverts and estimate gas.
  2. Send the transaction only if simulation succeeds.
  3. Wait for receipt and update UI state.

In wagmi, this usually means pairing useSimulateContract with useWriteContract, then tracking confirmation with useWaitForTransactionReceipt.

Why this matters:

  • You catch most revert reasons before a user pays gas.
  • You can show better error messages (e.g., “Insufficient allowance” vs “execution reverted”).
  • You can present more accurate gas estimates.

Also: treat the transaction lifecycle as a state machine in your UI—idle → signing → pending → confirmed/failed. Users tolerate slow blocks; they don’t tolerate ambiguity.

Token approvals: avoid the classic UX trap

ERC-20 approvals are where many apps lose users.

If your dapp needs a token transfer by a contract, you must:

  • Check allowance (allowance(owner, spender))
  • If insufficient, prompt approval
  • Only then prompt the main action (swap, stake, deposit)

A clean flow:

  • Button reads “Approve USDC” when approval is needed
  • After approval confirms, the button changes to “Deposit”

Opinionated recommendation: default to exact approvals unless your product has a strong reason for infinite approvals. Unlimited allowances reduce friction but increase blast radius if something goes wrong.

Chain switching and multi-chain design

Multi-chain support isn’t just “add more chain configs.” You need to think about:

  • Contract addresses per chain
  • Feature availability per chain
  • Different native currencies and gas UX

Use wagmi’s chain primitives and make your contract address selection explicit (e.g., a map keyed by chainId).

Practical approach:

  • Define contracts[chainId] = { token, router, vault }
  • Guard UI features behind “isSupportedChain” checks
  • When the chain is unsupported, show a single, clear call to action: switch to a supported network

Don’t scatter if (chainId === …) checks across components. Centralize it.

Error handling: decode, classify, and message

Users don’t care about “CALL_EXCEPTION.” They care about what to do next.

With viem, you can often decode errors more reliably, but you still need a strategy:

  • Classify common failures: user rejected signature, insufficient funds, revert with reason, RPC timeout
  • Display short, actionable messages
  • Log the full error object to your observability pipeline (Sentry, LogRocket)

A production app should also handle flaky RPCs gracefully:

  • Retry reads a limited number of times
  • Provide fallbacks for RPC endpoints
  • Avoid blocking the entire UI due to one failed query

Testing and production hardening

Before shipping, validate your core flows on:

  • At least one testnet (or a forked mainnet via Foundry/Hardhat)
  • Multiple wallets (MetaMask, Rabby, WalletConnect)
  • Slow networks and rate-limited RPC conditions

Hardening checklist:

  • Use strict TypeScript settings; treat address types carefully
  • Prefer bigint for onchain amounts (avoid floating point)
  • Normalize units with viem helpers (parse/format units)
  • Add analytics around funnel steps: connect → approve → transact → confirm

In our experience at ChainMagic Studio, most “Web3 frontend bugs” are actually state management and edge-case UX issues—wagmi + viem reduces the surface area, but you still need discipline.

Conclusion: build with hooks, think like a protocol engineer

wagmi and viem let you build modern Web3 frontends that feel closer to traditional product engineering: typed APIs, cached reads, explicit transaction lifecycles, and fewer mystery failures. The teams that succeed treat wallet connection and transaction flows as first-class UX, simulate before writing, centralize chain configuration, and invest in error handling.

If you do that, you’ll ship a frontend that users trust—and in Web3, trust is the product.