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 connections, reads/writes, events, and best practices for production apps.

Why wagmi + viem is the modern default

If you’re building an Ethereum (or EVM) dApp frontend in 2026, wagmi + viem is the most pragmatic stack: it’s type-safe, composable, React-friendly, and significantly less “magic” than older Web3.js-era approaches.

  • viem is the low-level, TypeScript-first EVM client: encoding/decoding, ABI interactions, transports, chain config, and RPC calls.
  • wagmi is the React layer: hooks for accounts, network state, contract reads/writes, caching, and wallet UX—built on top of viem.

Opinionated take: choose wagmi + viem when you want predictable behavior and strong typing, not a bag of side effects that break under load, network switches, or edge-case wallets.

Project setup: dependencies and structure

A typical Next.js or Vite React app will use:

  • wagmi for hooks + connectors
  • viem for clients and ABI utilities
  • @tanstack/react-query for caching (wagmi uses it)
  • a wallet UI (optional): RainbowKit, Web3Modal, or your own

A clean structure that scales:

  • src/web3/wagmi.ts (config + clients)
  • src/web3/abi/*.ts (typed ABIs)
  • src/web3/contracts.ts (addresses per chain)
  • src/components/* (UI)

Keep your chain IDs, RPCs, and contract addresses centralized. Most production bugs come from “address drift” and chain mismatch.

Configuring wagmi with viem clients

The backbone is a wagmi config with transports per chain.

Key concepts:

  • Chains: mainnet, Base, Arbitrum, etc.
  • Transport: HTTP or WebSocket RPC
  • Connectors: injected (MetaMask), WalletConnect, etc.

In production, prefer:

  • Dedicated RPC providers (Alchemy/Infura/QuickNode/Ankr) for reliability
  • Fallback transports to reduce outage risk

Practical guidance:

  • Use HTTP for reads/writes.
  • Add WebSocket only when you truly need realtime subscriptions (events/blocks). WS can be flaky on mobile networks.

Wallet connection: make it boring

Wallet connection is where UX goes to die if you over-engineer it.

With wagmi hooks you typically wire:

  • useAccount() to know if a user is connected and their address
  • useConnect() to start a connection flow
  • useDisconnect() for logout
  • useChainId() / useSwitchChain() to handle network mismatches

What “good” looks like:

  1. Detect wrong network and prompt a switch.
  2. Explain why the app needs a network.
  3. Don’t auto-popup wallets on page load.

Real example: if your dApp works on Base and Arbitrum, don’t hard-fail on Ethereum mainnet—offer a switch and show which networks are supported.

Contract reads: fast, typed, cacheable

Reads are the majority of frontend traffic. Optimize for:

  • minimal RPC calls
  • caching + invalidation
  • batching where possible

wagmi’s useReadContract is the default for single reads; useReadContracts can batch multiple calls.

Common patterns:

  • Token balance: balanceOf(address)
  • Allowance: allowance(owner, spender)
  • Protocol state: getReserves(), totalSupply(), slot0()

Best practice: treat reads like you would REST queries.

  • Cache for a short time (e.g., 5–15s) for UI responsiveness.
  • Invalidate after writes (more below).
  • Avoid polling aggressively; use block-based refresh if needed.

If you’re displaying onchain prices or pool state (e.g., Uniswap V3 slot0()), you’ll want to refresh on new blocks rather than every second. That’s cheaper and less error-prone.

Contract writes: simulate first, then send

The single biggest reliability improvement in modern dApps is simulation before transaction submission.

Why?

  • You catch reverts early and can show meaningful errors.
  • You estimate gas more accurately.
  • You avoid spamming the wallet with transactions that will fail.

With viem + wagmi, the flow is typically:

  1. Simulate the contract call (dry-run).
  2. Write the transaction.
  3. Wait for receipt.
  4. Invalidate reads.

Example: ERC-20 approve + deposit

  • First, check allowance.
  • If insufficient, approve(spender, amount).
  • Then call deposit(amount).

This sounds simple, but production dApps must handle:

  • user rejecting a signature
  • nonce issues on congested networks
  • “replacement transaction underpriced”
  • chain switching mid-flow

Make writes resilient:

  • Always show transaction state (pending/confirmed/failed).
  • Link to a block explorer per chain.
  • Keep UI idempotent: don’t let users submit the same write 5 times.

Handling events and realtime updates

Event-driven UX is powerful (and often overused).

Use events when:

  • you need to reflect protocol actions not initiated by the current user
  • you’re building a live feed, order book, or liquidation dashboard

Otherwise, prefer simpler strategies:

  • refetch on new blocks
  • refetch after confirmed writes

If you do use events:

  • filter narrowly (contract address + event signature)
  • consider an indexer (The Graph, Envio, Ponder) for historical queries

Opinionated recommendation: don’t build your app’s core state on client-side event subscriptions. Mobile browsers sleep tabs, WS disconnects, and you’ll end up with inconsistent state. Use events for “nice to have” realtime garnish; use indexed data or deterministic reads for truth.

Multi-chain and address management

Multi-chain is now the norm, but it’s a foot-gun.

Do this:

  • Maintain a mapping: { [chainId]: { ContractName: address } }
  • Validate at runtime: if no address for current chain, disable actions
  • Use useSwitchChain() to guide users to supported networks

Don’t do this:

  • Hardcode addresses inside components
  • Assume chain ID from wallet equals your app’s default

Also, pay attention to decimals and native currency differences when displaying balances and fees.

Error handling and “human” messages

Raw RPC errors are hostile. Normalize them.

Common cases to map:

  • User rejected signature
  • Insufficient funds for gas
  • Execution reverted (surface a friendly cause)
  • Wrong network

viem errors are structured; leverage that. Your goal is a message a non-crypto user can understand:

  • “Transaction rejected in wallet”
  • “You need ETH for gas on Arbitrum”
  • “This mint is sold out” (instead of revert SoldOut())

If you control the contract, prefer custom errors (error SoldOut();) and decode them on the frontend for clean UX.

Performance and security best practices

A few battle-tested rules:

  1. Never trust the frontend: all important checks must be enforced in the contract.
  2. Use exact units: format for display, but store/compute using bigint.
  3. Keep RPC calls lean: batch reads, avoid heavy polling.
  4. Guard writes: disable buttons when prerequisites aren’t met.
  5. Don’t leak API keys: use public keys only, or proxy through your backend.

If you’re building a serious DeFi UI, consider pairing wagmi/viem with:

  • an indexer for historical and aggregate views
  • a server component or backend for offchain metadata
  • feature flags for chain rollouts

Conclusion: build dApps like real software

wagmi and viem push Web3 frontend development in the right direction: typed clients, predictable hooks, and production-grade patterns like simulation-before-send.

The winning approach is to treat onchain interactions as a disciplined system:

  • reads are cached queries
  • writes are state transitions with clear UX
  • events are optional enhancements, not your source of truth

Get those fundamentals right, and your Web3 frontend stops feeling like a demo—and starts behaving like a product users can trust.