Web3 Development · 5 min read ·

Web3 Frontends with wagmi + viem: A Practical Guide

Build fast, type-safe Web3 frontends using wagmi hooks and viem clients, with best practices for reads, writes, and chain switching.

Web3 frontend development is where product reality meets blockchain reality: wallets are flaky, networks change, RPCs rate-limit, and users expect the app to feel as smooth as any Web2 product. The best modern stack for Ethereum and EVM chains is wagmi (React hooks + state + connectors) paired with viem (type-safe, low-level EVM client).

If you’re still using older “everything-and-the-kitchen-sink” libraries, you’re likely paying in bundle size, unclear abstractions, and runtime errors that TypeScript could have caught. wagmi + viem is opinionated in the right places and composable everywhere else.

Why wagmi + viem (and why now)

wagmi gives you React-friendly primitives: connect wallet, track account state, sign messages, simulate and send transactions, read contracts, watch events. It integrates cleanly with caching solutions like TanStack Query.

viem is the engine underneath: it handles transport, encoding/decoding, signing, and contract interactions with strong typing. It’s designed to be tree-shakeable and explicit—meaning you choose your transport, chains, and clients, which matters for performance and reliability.

In practice:

  • Use wagmi for UI state and lifecycle hooks.
  • Use viem for typed contract calls, client setup, and custom flows.

Project setup: config, chains, transports

A solid wagmi setup starts with defining supported chains and transports. In production, you want redundancy (multiple RPCs) and clear separation between public reads and wallet writes.

Key decisions:

  1. Chains: explicitly list the networks you support (e.g., mainnet, Base, Arbitrum). Avoid “support everything” unless you have a reason.
  2. Transport: http() is simplest; fallback() improves resilience across RPC providers.
  3. SSR (Next.js): configure wagmi for SSR if you render on the server, and avoid reading wallet state during server render.

Practical recommendation: keep reads fast and cheap with a public client; keep writes tied to the user’s wallet client.

Wallet connection UX: connectors and state

Your “Connect Wallet” flow is not a button—it’s a state machine. wagmi exposes this state clearly, which helps you build a UX that doesn’t lie.

What to handle explicitly:

  • Disconnected: show connect options.
  • Connecting: disable repeated clicks.
  • Connected: show address + network.
  • Unsupported chain: show “Switch network” CTA.
  • Reconnecting: on page refresh, hydrate state without flicker.

Connectors to consider:

  • Injected (MetaMask, Brave)
  • WalletConnect (mobile, multi-wallet)
  • Coinbase Wallet (useful for retail-focused apps)

Opinionated take: if you’re building a consumer dapp, don’t ship with only Injected. WalletConnect is table stakes.

Type-safe contract reads: keep them deterministic

Reads should be:

  • Cached (avoid hammering RPCs)
  • Deterministic (same input → same output)
  • Network-aware (don’t accidentally read mainnet when user is on Base)

wagmi’s contract read hooks work best when paired with an ABI typed via tooling like abitype or codegen from your contract artifacts. With viem’s strong typing, your function names and argument types are validated at compile time.

Patterns that scale:

  • Prefer single-purpose hooks per feature (e.g., useTokenBalance, useVaultPosition).
  • Use multicall for dashboards (balances, allowances, positions) to reduce RPC round-trips.
  • Be explicit about chainId when your app supports multiple networks.

Example scenario: a token approval UI.

  • Read allowance for (owner, spender)
  • Read balance
  • Read decimals/symbol once and cache

You’ll end up with a UI that never guesses.

Writes done right: simulate, then send

The fastest way to ship a buggy dapp is to call writeContract directly and hope for the best. A better flow is:

  1. Simulate the transaction (catches reverts, estimates gas, validates args)
  2. Send the transaction
  3. Wait for receipt (confirm mined)
  4. Invalidate/refetch reads (update UI)

wagmi supports this flow cleanly, and viem’s simulation makes failures explainable. This is especially important for:

  • complex DeFi interactions
  • ERC-20 approvals with nonstandard behavior
  • contracts that revert with custom errors

Practical insight: show users actionable errors. Don’t display raw “execution reverted.” Map common failures (insufficient balance, slippage, paused contract) to clear messages.

Network switching and multi-chain apps

Multi-chain UX is where many frontends fall apart. Users don’t think in chain IDs—they think in “the app works.”

Best practices:

  • Detect unsupported network and provide a one-click switch.
  • If your protocol is deployed on multiple chains, present chain selection as a product choice (fees, speed), not a dev detail.
  • If your protocol is only on one chain, don’t pretend: lock the app to that chain and guide switching.

wagmi’s network hooks expose chain, chains, and switching methods so you can build a predictable flow.

Events and real-time updates: avoid polling everything

Polling every 3 seconds is an easy habit—and a costly one at scale. For reactive UX (positions updating after a swap, new deposits, order fills), use events where possible.

Approaches:

  • Watch contract events for on-chain state changes.
  • Subscribe to new blocks and refresh only what depends on block state.
  • Use targeted invalidation: refetch the specific queries affected by a transaction.

Caveat: event subscriptions depend on your RPC/provider quality. For production-grade apps, consider a fallback strategy:

  • Try websocket subscriptions
  • If unavailable, fall back to block polling at a reasonable interval

Performance, reliability, and production hardening

A Web3 frontend is only as good as its RPCs and caching strategy.

Concrete guidance:

  • Use fallback() transports across at least two providers (e.g., Alchemy + public RPC).
  • Set sane query caching: balances can be moderately stale; transaction receipts should be aggressive.
  • Avoid re-render storms: keep hook usage scoped to components that need it.
  • Don’t over-fetch: multicall for dashboards; single calls for detail pages.

Security and correctness notes:

  • Validate addresses with viem utilities before calling contracts.
  • Never trust user-provided chain/contract addresses; keep an allowlist per chain.
  • Treat approve as a security-sensitive action: consider “approve exact” vs “infinite approval” and be explicit in the UI.

A concrete architecture that scales

A proven structure for medium-to-large dapps:

  • lib/chains.ts: supported chains + contract addresses per chain
  • lib/wagmi.ts: wagmi config, connectors, transports
  • lib/abi/: versioned ABIs (or generated types)
  • features/<domain>/hooks/: domain hooks wrapping wagmi reads/writes
  • features/<domain>/components/: UI components consuming those hooks

This avoids the “hooks everywhere” spaghetti and makes audits and refactors easier.

Conclusion

wagmi + viem is the current best-in-class approach for EVM Web3 frontends because it forces clarity: explicit clients, explicit chains, and type-safe contract interactions—without fighting React. The winning pattern is consistent across most apps:

  • Use wagmi for connection state and ergonomic hooks
  • Use viem for typed contract calls and reliable transport
  • Simulate before sending transactions
  • Cache reads, invalidate precisely, and subscribe to what matters

If you adopt these practices early, you’ll ship a dapp that feels fast, fails gracefully, and stays maintainable as your protocol—and your user base—grows.