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, events, caching, and production pitfalls.

Building Web3 frontends used to mean stitching together wallet connectors, provider quirks, and brittle Ethers.js abstractions. Today, the most pragmatic stack for React apps is wagmi (React hooks + wallet UX) paired with viem (a fast, typed Ethereum client). This combo is opinionated in the right places: it makes the “happy path” easy while still letting you drop down to low-level RPC calls when needed.

This article walks through how to structure a production-grade frontend with wagmi and viem: configuration, reads/writes, events, performance, and the gotchas teams usually hit.

Why wagmi + viem (and when it’s the wrong choice)

wagmi focuses on stateful React primitives: connect buttons, account state, network switching, read/write hooks, and excellent integration with TanStack Query. viem is the engine: typed ABI interactions, multicall, transport configuration, and predictable behavior across chains.

Why this matters in practice:

  • Type-safety reduces UI bugs. viem’s ABI typing catches “string vs bigint” and function signature mismatches early.
  • Less magic around providers. You can explicitly define transports, fallback RPCs, and chain configs.
  • Better performance for reads. Multicall + query caching becomes a default pattern.

When it’s not ideal:

  • If you’re not using React, wagmi’s hook model won’t help.
  • If your app is mostly offchain and only occasionally signs messages, a lightweight signer library plus direct RPC might be simpler.

Project setup: modern defaults that won’t hurt later

In a typical Next.js or Vite React project, you’ll install wagmi, viem, and a connector UI layer (many teams use RainbowKit). Even if you don’t want a full “wallet modal,” wagmi connectors still simplify account management.

A key early decision: where your public reads come from.

  • Public reads (balances, prices, positions) should usually use public RPCs (your own or a provider) via viem transports.
  • Writes (transactions) should use the wallet client from the user’s wallet.

In wagmi v2, the core configuration is built with createConfig, chain definitions, and transports:

  • Define supported chains (e.g., Ethereum, Base, Arbitrum).
  • Use http() transports with fallback() for reliability.
  • Add connectors (Injected, WalletConnect) with sane metadata.

Opinionated tip: don’t ship with a single RPC URL. Redundancy saves you from “it’s down for some users” incidents.

Wallet UX: connection, chain switching, and account state

A good wallet UX is mostly about eliminating surprise states:

  • disconnected
  • connected but wrong network
  • connected but account changed
  • wallet locked
  • user rejected request

wagmi gives you the building blocks:

  • useAccount() for address and connection status
  • useConnect() / useDisconnect() for lifecycle
  • useChainId() and useSwitchChain() for network correctness

Practical pattern:

  1. Gate writes behind network checks. If the user is on the wrong chain, show a “Switch to Base” button rather than letting the transaction fail.
  2. Listen for account changes. If the user switches accounts mid-flow, invalidate cached queries and reset form state.
  3. Handle rejections explicitly. Wallet “User rejected” is not an error; it’s a user action. Treat it as a cancelled flow.

Contract reads: typed ABIs, caching, and multicall

Most Web3 UIs are read-heavy. The difference between a snappy app and a sluggish one is often just how you batch and cache reads.

With wagmi, you typically use useReadContract() for single reads and useReadContracts() for batching. Under the hood, viem enables multicall-style behavior where available.

Best practices:

  • Prefer useReadContracts() for dashboards. If you need balanceOf, allowance, and decimals, fetch them together.
  • Use stable query keys. wagmi integrates with TanStack Query; ensure your inputs (address, chainId) are part of the query key.
  • Be consistent with bigint. viem returns integers as bigint. Convert at the display layer using formatting helpers (e.g., formatUnits) and keep internal math in bigint to avoid rounding errors.

Real example: token approvals

  • Read allowance(owner, spender)
  • If allowance < required amount, show “Approve”
  • Otherwise show “Swap” / “Deposit” This flow depends on fast reads and reliable invalidation after writes.

Contract writes: simulation-first transactions

The most common production failure mode is prompting users to sign transactions that will revert. wagmi + viem supports a safer pattern: simulate first, then write.

Recommended flow:

  1. Simulate the contract call with current args and account.
  2. If simulation passes, send the transaction.
  3. Wait for receipt and then invalidate relevant reads.

In wagmi, this maps cleanly to:

  • useSimulateContract() (or simulateContract via the public client)
  • useWriteContract()
  • useWaitForTransactionReceipt()

Benefits:

  • You catch reverts caused by missing approvals, paused contracts, max slippage, or invalid routes.
  • You can show clearer errors (e.g., “Insufficient allowance”) instead of generic “execution reverted.”

Opinionated tip: always set UI state based on receipt confirmations, not just “transaction submitted.” Submission is not success.

Events and real-time UX: polling vs watch

A responsive UI often needs to react to onchain changes:

  • a deposit mined
  • an NFT transferred
  • a liquidation event fired

You have two broad strategies:

  • Polling reads (simple, reliable, more RPC load)
  • Watching logs (more real-time, can be tricky across providers)

viem supports watchContractEvent and log queries. In practice:

  • Use event watching for “live feeds” or notifications.
  • Use polling or receipt-based invalidation for “your position/balance” UI.

Practical approach that scales:

  • After a write, wait for the receipt and then refetch key reads.
  • For global updates, poll every 10–30 seconds rather than trying to maintain perfect websocket subscriptions across flaky mobile wallets.

Production pitfalls (and how to avoid them)

A few issues show up repeatedly in real deployments:

  1. SSR and hydration issues (Next.js). Wallet state is client-only. Ensure wallet UI components render after mount or use dynamic import to avoid mismatch.

  2. Chain mismatch across connectors. Some wallets report chain state inconsistently on initial load. Always treat chainId as asynchronous: show a “Checking network…” state.

  3. Decimals and formatting bugs. Don’t parse floats for token math. Keep values in bigint and format at the edge.

  4. RPC rate limits. If your UI spams reads on every rerender, you’ll hit 429s. Lean on query caching, batching, and explicit refetch triggers.

  5. Allowance UX. Users hate repeated approvals. Consider “max approve” toggles, but be explicit about risks. For higher-stakes apps, recommend exact approvals by default.

Conclusion: the sane path for Web3 frontends

wagmi + viem is the most developer-friendly way to build modern Web3 frontends in React: wagmi handles wallet state and hook ergonomics; viem provides a fast, typed, explicit client for reads, writes, and events. The winning production pattern is consistent: batch reads, simulate before writes, wait for receipts, and invalidate caches deliberately.

If you adopt one mindset shift, make it this: treat the blockchain like a slow, adversarial database. Your frontend’s job is to minimize unnecessary calls, anticipate failure, and make state transitions legible to users. wagmi and viem give you the right primitives—use them with discipline.