Web3 Development · 5 min read ·

Web3 Frontends with wagmi + viem: A Practical Guide

Build fast, reliable Ethereum dapp frontends using wagmi hooks and viem clients, with modern patterns for reads, writes, and UX.

Web3 Frontend Development with wagmi and viem

Most Web3 frontends fail for boring reasons: flaky RPC calls, inconsistent wallet behavior, and state that drifts out of sync with the chain. The wagmi + viem stack is popular because it addresses those problems with a clean mental model: viem is the low-level, type-safe EVM client, and wagmi is the React layer that turns wallet + chain interactions into predictable hooks.

If you’re building a production dapp (not a hackathon demo), the combination is hard to beat: smaller bundle sizes than legacy stacks, better TypeScript ergonomics, and an architecture that pushes you toward correct patterns.

What wagmi and viem actually do (and why it matters)

  • viem: Think of it as your “ethers replacement” for modern frontends. It provides publicClient for reads (no wallet needed) and walletClient for signed actions (needs a connected account). It’s built to be strict about types (addresses, ABIs, event args), which prevents an entire class of runtime bugs.
  • wagmi: A set of React hooks and connectors built on top of viem. It standardizes wallet connections, caching, and reactive contract state. You get hooks like useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt, etc.

The key design win: separate read paths from write paths. Reads are cheap, cacheable, and should not depend on a wallet. Writes require explicit user intent and wallet state.

Project setup: the minimal production baseline

In a typical Next.js or Vite React app, you’ll wire:

  1. Chains + transports (RPC endpoints)
  2. Connectors (Injected, WalletConnect, Coinbase, etc.)
  3. Query caching (wagmi uses TanStack Query under the hood)

A pragmatic baseline looks like:

  • Define supported chains (e.g., mainnet, base, arbitrum, sepolia).
  • Use multiple RPC providers where possible (Alchemy/Infura + fallback) to reduce outages.
  • Configure WalletConnect for mobile reliability.

Conceptually:

  • createConfig({ chains, transports, connectors })
  • Wrap your app with <WagmiProvider config={config}> and <QueryClientProvider>.

Opinionated note: don’t couple your app to a single chain even if you launch on one. Users will arrive on the “wrong” network, and your frontend should handle it gracefully.

Wallet connection UX: predictable state beats fancy modals

The two hooks you’ll touch constantly:

  • useAccount() → { address, isConnected, chain }
  • useConnect() / useDisconnect() → connect flows

A good UX pattern:

  • Show a “Connect” button when !isConnected.
  • After connect, show a shortened address and chain.
  • If the chain is unsupported, show a “Switch network” call-to-action.

Two practical tips that prevent support tickets:

  1. Treat connection as session state, not app state. Users refresh, browsers sleep, mobile wallets hop apps. wagmi handles reconnection, but your UI should assume the wallet can disappear.
  2. Make network switching explicit. Use useSwitchChain() and show a clear error when the user is on the wrong chain.

Contract reads: fast, cached, and wallet-free

For reads, you typically want:

  • No wallet dependency
  • Automatic refetching when parameters change
  • Caching to avoid hammering RPCs

wagmi’s useReadContract gives you exactly that.

Example use cases that map cleanly to reads:

  • balanceOf(address) for ERC-20
  • allowance(owner, spender)
  • protocol config values (fees, caps)

Best practices:

  • Gate reads with enabled. If address is undefined, don’t call balanceOf.
  • Avoid polling by default. Prefer refetching on app focus or after writes.
  • Use multicall for dashboards. For pages that load many read calls, wagmi/viem can batch via multicall on supported chains, reducing latency and RPC load.

If you’re building a portfolio-style UI, multicall is the difference between “snappy” and “stutters on every rerender.”

Contract writes: simulate first, then send

Writes are where frontends get users rekt: wrong calldata, wrong chain, wrong gas assumptions.

A robust pattern is:

  1. Simulate the transaction (estimate + validate)
  2. Send the transaction
  3. Wait for receipt
  4. Invalidate/refetch relevant reads

In wagmi, writes are typically handled with useWriteContract() and then useWaitForTransactionReceipt().

Why simulation matters:

  • Catch reverts before the wallet prompt when possible
  • Provide better error messages (“insufficient allowance”, “sale not active”)
  • Reduce user frustration from failed transactions

A common ERC-20 flow:

  • Read allowance(owner, spender)
  • If allowance is insufficient, prompt approve(spender, amount)
  • After approval confirms, call the protocol function (e.g., deposit(amount))

This sequencing is exactly where wagmi’s hook composition shines.

Handling errors like a grown-up: decode, categorize, message

Your users do not care that the error came from “CALL_EXCEPTION.” They care what to do next.

Practical error handling guidance:

  • User rejected request: show “Transaction canceled” and stop.
  • Wrong network: show “Switch to Base” (or whatever chain).
  • Revert with reason: show the decoded reason, but also translate it into action.

viem is good at decoding revert data when ABIs include custom errors. If your contracts use Solidity custom errors (they should), your frontend can surface precise messages.

Opinionated note: if you ship contracts without meaningful custom errors, you’re choosing worse UX.

Event-driven UI: receipts, logs, and “real” confirmations

A wallet returning a transaction hash is not success. Users need a confirmed state.

Use useWaitForTransactionReceipt({ hash }) to drive:

  • Loading states (“Confirming…”)
  • Post-confirmation actions (toast, navigate, refetch)

For real-time updates (mint counters, order fills), you can subscribe to events using viem’s log utilities (or wagmi equivalents depending on your version). The pattern:

  • Subscribe on the correct chain
  • Filter to the relevant contract + event
  • Update local UI state or invalidate cached queries

This is how you avoid naive polling loops.

Performance and reliability: RPC strategy is part of frontend dev

Frontends are often the only layer users see, so reliability is your job—even if the issue is an RPC provider.

What we recommend in production:

  • Multiple transports with fallback when possible
  • Rate-limit awareness (dashboards can burn through quotas)
  • Separate read/write RPCs: reads can go to a high-throughput provider; writes depend on the wallet anyway
  • Strict chain configuration: don’t let the app guess chain IDs

Also: don’t ship a frontend that breaks when the user has multiple wallets installed. Test with MetaMask + Coinbase Wallet + a WalletConnect wallet.

Testing the critical paths

At minimum, test:

  • Connect/disconnect flow
  • Wrong network + switch
  • Read rendering with address undefined
  • Approve + action sequence
  • Error cases (revert, rejection)

Use a testnet like Sepolia for manual QA, and consider Playwright for smoke tests. The goal is not perfect coverage; it’s preventing regressions in the flows that make or lose money.

Conclusion: wagmi + viem is the modern default

wagmi and viem push you toward the right architecture: wallet state is explicit, reads are cacheable and independent, writes are structured and confirmable. If you care about UX, reliability, and TypeScript correctness, this stack is a strong default for EVM dapps.

The big takeaway: treat your Web3 frontend like a serious distributed system client, not a simple UI. Simulate transactions, handle networks deliberately, confirm on-chain outcomes, and invest in RPC resilience. Do that, and wagmi + viem will feel less like “Web3 tooling” and more like a real application framework.