Web3 Frontend and dApp Interface Development
User clicks "Connect Wallet" — MetaMask opens, confirms — and nothing happens. Or worse: transaction sent, UI hangs on "pending" forever because the event listener broke when switching networks. Web3 frontend isn't just React + API calls. It's wallet interaction, nodes, blockchain reorgs, and state that doesn't belong to your server.
Modern Stack: wagmi v2 + viem
wagmi v2 is React hooks for EVM chain interaction. viem is the low-level TypeScript client that replaced ethers.js in most new projects. The wagmi + viem combo gives typed access to contracts, wallets, transactions.
Type safety through viem: ABI passed as const assertion, and TypeScript knows argument and return types at compile time. Contract errors are caught before runtime.
Wallet Connection: RainbowKit and WalletConnect
RainbowKit is UI library over wagmi for wallet modal. Supports MetaMask, WalletConnect v2, Coinbase Wallet, Phantom, Safe and dozens more. ConnectKit is an alternative with different design. Both properly handle wallet detection, deep links for mobile, and EIP-6963 (multi-injected wallet discovery).
WalletConnect v2 is protocol for dApp-to-mobile-wallet connection via QR or deep link. Requires ProjectID from cloud.walletconnect.com. On integration: WalletConnect v1 is deprecated, migration to v2 is mandatory.
Main UX case that breaks: user connected wallet on Ethereum Mainnet, but contract lives on Arbitrum. Need to:
- Detect wrong network
- Offer switch via
wallet_switchEthereumChain - If network not added —
wallet_addEthereumChain - Wait for switch confirmation before sending transaction
wagmi handles this via useSwitchChain(), but UX flow must be designed explicitly — automatic switching without explanation scares users.
Multichain: One dApp, Several Networks
wagmi supports multichain config. Contracts on different networks have different addresses, different ABIs (on upgrades), different block times. Config structure:
const config = createConfig({
chains: [mainnet, arbitrum, optimism, polygon, base],
connectors: [injected(), walletConnect({ projectId }), coinbaseWallet()],
transports: {
[mainnet.id]: http(alchemyUrl),
[arbitrum.id]: http(arbitrumRpcUrl),
},
})
Contract addresses in typed map by chainId — don't hardcode separately for each network.
Transaction Handling: States and Errors
Transaction goes through states: idle → pending (wallet) → submitted → confirming → confirmed. Each transition can break with an error.
Typical errors and handling:
-
UserRejectedRequestError— user rejected in wallet. Don't show as "server error", just reset state. -
InsufficientFundsError— not enough ETH/native token for gas. Show specific amount. -
ContractFunctionRevertedError— contract reverted. viem parses custom errors from ABI and returns typed error with args. - Transaction dropped/replaced — user accelerated with same nonce.
useWaitForTransactionReceipthandles viaonReplacedcallback.
Gas estimation failures need capturing before transaction send. estimateGas() in viem throws error with revert reason if transaction would fail — show this to user better than letting them waste gas.
Reading Data: Multicall and Caching
One RPC request per balanceOf loading page with 20 tokens means 20 requests. wagmi automatically batches useReadContract calls via Multicall3 contract (deployed on all major networks at one address). Reduces RPC load and speeds up loading.
React Query underneath wagmi ensures caching and automatic refetch. staleTime and refetchInterval config is important for balance between data freshness and RPC load.
For complex queries — historical data, event aggregation — The Graph subgraph or Ponder. GraphQL query to subgraph instead of scanning thousands of blocks via RPC.
ENS and Identity
normalize from viem for ENS — resolve .eth addresses and reverse lookup (address → ENS name). Show vitalik.eth instead of 0xd8dA... where possible. Avatar resolution — ENS avatar via getEnsAvatar().
Signatures and Authentication: Sign-In with Ethereum
EIP-4361 (SIWE) — authentication standard via wallet signature without transaction. Server generates nonce → user signs message via personal_sign → server verifies signature. Replace username/password for Web3 apps. siwe npm package on client and server.
Off-chain operation signatures (EIP-712 typed data) — structured data that MetaMask displays human-readable instead of hex blob. Use for approve, order signatures in DEX, permit (ERC-2612).
Performance and Optimization
wagmi + viem + RainbowKit bundle weighs ~200–400kb gzipped. For NextJS use dynamic imports with ssr: false for all wallet-dependent components. SSR + web3 provider hydration is a known state mismatch issue. Pattern: render connected state only on client.
Stack and Tools
Core: React 18 + TypeScript, wagmi v2, viem, RainbowKit or ConnectKit Alternatives: ethers.js v6 (legacy projects), web3.js (not recommended for new) Data queries: React Query, The Graph (subgraph + Apollo), SWR Testing: Vitest + Testing Library, anvil (local node) for integration tests Dev networks: anvil (Foundry), Hardhat Network with mainnet fork
Timelines
- Basic dApp (read data + one transaction): 2–3 weeks
- Full DeFi interface (swap, stake, dashboard): 6–10 weeks
- NFT marketplace UI: 4–8 weeks
- Custom multichain wallet: 8–14 weeks







