Web3 Development · 5 min read ·

Web3 Frontends with wagmi + viem: A Practical Guide

Learn how to build modern Ethereum app frontends using wagmi hooks and viem clients for fast, type-safe wallet connections, reads, and writes.

Web3 frontend development with wagmi and viem

Most Web3 frontends fail for boring reasons: flaky wallet connections, inconsistent chain config, slow RPC reads, and contract calls that are hard to type and harder to debug. The modern fix is a clear separation of concerns:

  • wagmi for React-first wallet UX and state management (connect, sign, switch chains, hooks)
  • viem for fast, type-safe Ethereum JSON-RPC clients (reads, writes, events, simulation)

Together they replace the older “ethers everywhere” approach with something more modular, more type-safe, and easier to maintain at scale.

Why wagmi + viem is the current best default

A good Web3 frontend needs to do four things reliably:

  1. Connect wallets (in-app state, connectors, session persistence)
  2. Read chain data efficiently (cache, retries, batching, multi-chain)
  3. Write transactions safely (simulate first, handle reverts, track confirmations)
  4. Keep UI state coherent (loading, errors, chain mismatch, optimistic states)

wagmi handles (1) and much of (4) through opinionated React hooks. viem excels at (2) and (3) with a small surface area and strong TypeScript types.

The opinionated take: if you’re starting a new React/Web3 app in 2026, wagmi + viem should be your default unless you have a hard constraint (non-React frontend, custom wallet stack, or deep protocol-specific client requirements).

Project setup: the pieces that matter

A typical stack:

  • React + Next.js (or Vite)
  • wagmi for hooks
  • viem for clients/transports
  • Wallet UI: RainbowKit, ConnectKit, or a custom modal
  • TanStack Query (wagmi uses it under the hood for caching)

Key design choice: configure chains and transports once, then consume via hooks everywhere.

In wagmi v2, you create a config that wires:

  • chains (e.g., mainnet, base, arbitrum)
  • transports (HTTP/WebSocket per chain)
  • connectors (Injected, WalletConnect, Coinbase Wallet, etc.)

Then wrap your app with WagmiProvider (and typically QueryClientProvider). This centralizes chain + RPC policy so you don’t scatter RPC URLs across components.

Chain configuration and RPC strategy (don’t wing this)

RPC reliability is the hidden cost center of Web3 frontends. Two practical rules:

  • Use at least two RPC providers in production (primary + fallback). Outages happen.
  • Prefer HTTP for most reads; add WebSocket only for real-time UX (pending tx feeds, live events).

viem transports make this straightforward. You can route by chain and swap providers without rewriting app code.

Example policy that scales:

  • Mainnet: paid provider (Alchemy/Infura/QuickNode) + public fallback
  • L2s (Base/OP/Arbitrum): paid provider + chain public endpoint fallback
  • Local/dev: Anvil/Hardhat node

Reading contract data with viem-backed hooks

The most common frontend bug is mismatching ABI types and arguments. viem’s typing helps prevent that.

In wagmi, reads typically use useReadContract and useReadContracts (batch). Under the hood, wagmi relies on viem clients.

Practical patterns:

  • Batch reads for dashboards: balances, allowances, positions—use multicall via useReadContracts.
  • Gate reads behind prerequisites: don’t fire on page load if the user isn’t connected or is on the wrong chain.
  • Treat reads as cacheable data: rely on query keys and staleTime rather than manual state.

A concrete example: an ERC-20 token row often needs balanceOf(user), decimals(), and symbol(). Batch those three calls to avoid waterfall latency.

Writing transactions: simulate, then send

A polished dapp doesn’t just “send a transaction.” It:

  1. Checks chain + account
  2. Simulates the call (catches reverts early, estimates gas)
  3. Sends the transaction
  4. Tracks confirmations
  5. Invalidates relevant reads to refresh UI

With wagmi + viem, the simulation-first flow is a best practice, not a nice-to-have.

Typical approach:

  • useSimulateContract to validate args and preflight the call
  • useWriteContract to submit
  • useWaitForTransactionReceipt to confirm

Why simulation matters: it surfaces errors like “ERC20: insufficient allowance” before the user pays gas, and it produces a request object that can be handed to the writer.

Wallet UX: chain switching and “wrong network” states

Real users are on the wrong chain constantly. Your UI should treat this as a first-class path.

Minimum viable UX:

  • Detect current chain via useChainId() or useAccount() state
  • If wrong: show a single CTA button that calls switchChain
  • If switching fails (some wallets reject programmatic switching): show manual instructions and the correct chain parameters

Avoid dark patterns:

  • Don’t auto-switch chains on page load without asking
  • Don’t spam connection modals
  • Don’t hide errors—surface the wallet’s rejection reason

Events and real-time updates (use sparingly)

Watching events is seductive—and expensive. WebSocket subscriptions can be flaky on mobile and certain corporate networks.

Use event watching for:

  • Live order fills
  • New bids/offers
  • Real-time game state

Avoid it for:

  • Basic portfolio pages (polling + caching is enough)
  • Anything that can be derived from a periodic refetch

If you do use it, keep the subscription scope tight and handle reconnect logic gracefully.

Type safety: generate contract types or regret it later

The fastest way to accumulate frontend debt is copy-pasting ABIs and hand-typing addresses.

Do this instead:

  • Store ABIs in a single package/module
  • Generate typed contract wrappers (or at minimum typed ABIs) during build
  • Maintain a chain/address map keyed by chainId

A practical pattern for multi-chain deployments:

  • contracts.ts exports { chainId: { Token: { address, abi }}}
  • Components accept a chainId (or derive it) and pick the correct address

This prevents the classic mistake: calling the right ABI on the wrong address on the wrong chain.

Common pitfalls (and how to avoid them)

  • Mixing bigint and number: viem uses bigint by design. Keep values as bigint until final formatting.
  • Forgetting decimals: format token amounts with formatUnits(value, decimals) and parse with parseUnits.
  • Not invalidating reads after writes: after a successful tx, invalidate queries for balances/allowances/positions.
  • Over-fetching on every render: ensure hooks have stable args and use enabled flags.
  • Assuming one connector works everywhere: support Injected + WalletConnect at minimum.

Conclusion: a maintainable Web3 frontend is mostly good plumbing

wagmi and viem are not just trendy libraries—they encode hard-won lessons about wallet state, RPC reality, and type-safe contract interaction. Use wagmi to standardize connection and UX flows, use viem to make reads/writes fast and predictable, and invest early in chain config + typed ABIs.

If you do those three things, the “Web3 part” of your frontend becomes boring—in the best possible way—and your team can focus on product: onboarding, conversion, and features users actually care about.