Back to Blog
BlockchainReactWeb3WagmiEthereum

Web3 Wallet Integration in React: A Complete Guide

Learn how to integrate Web3 wallets into your React application using Wagmi and Viem. Connect MetaMask, WalletConnect, and more with modern best practices.

Nawab Khairuzzaman7 min read
Share:
Web3 Wallet Integration in React: A Complete Guide

Web3 applications live or die by their wallet integration. A smooth connection experience can make the difference between a user staying or bouncing. In this guide, I'll walk you through building a production-ready wallet integration using the modern Wagmi + Viem stack.

Why Wagmi + Viem?

If you're starting a new Web3 project in 2025, Wagmi is the clear choice. Here's why:

LibraryProsCons
Wagmi + ViemType-safe, React hooks, active developmentLearning curve if coming from ethers
ethers.jsMature, well-documentedLarger bundle, less React-native
web3.jsLegacy supportOutdated patterns, larger bundle

"Wagmi provides React hooks that handle caching, request deduplication, and automatic refetching out of the box."

Setting Up Your Project

First, install the necessary dependencies:

bash
npm install wagmi viem @tanstack/react-query

Configure the Wagmi Client

Create a configuration file to set up your chains and connectors:

typescript
// src/config/wagmi.ts
import { http, createConfig } from 'wagmi'
import { mainnet, sepolia, polygon } from 'wagmi/chains'
import { injected, walletConnect, coinbaseWallet } from 'wagmi/connectors'
 
const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_ID!
 
export const config = createConfig({
  chains: [mainnet, sepolia, polygon],
  connectors: [
    injected(),
    walletConnect({ projectId }),
    coinbaseWallet({ appName: 'My dApp' }),
  ],
  transports: {
    [mainnet.id]: http(),
    [sepolia.id]: http(),
    [polygon.id]: http(),
  },
})

Set Up Providers

Wrap your application with the necessary providers:

tsx
// src/providers/Web3Provider.tsx
'use client'
 
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { config } from '@/config/wagmi'
 
const queryClient = new QueryClient()
 
export function Web3Provider({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </WagmiProvider>
  )
}

Building the Connect Button

Now let's create a reusable wallet connect component:

tsx
// src/components/ConnectButton.tsx
'use client'
 
import { useAccount, useConnect, useDisconnect } from 'wagmi'
 
export function ConnectButton() {
  const { address, isConnected } = useAccount()
  const { connect, connectors, isPending } = useConnect()
  const { disconnect } = useDisconnect()
 
  if (isConnected) {
    return (
      <div className="flex items-center gap-4">
        <span className="font-mono text-sm">
          {address?.slice(0, 6)}...{address?.slice(-4)}
        </span>
        <button
          onClick={() => disconnect()}
          className="px-4 py-2 bg-red-500 text-white rounded-lg"
        >
          Disconnect
        </button>
      </div>
    )
  }
 
  return (
    <div className="flex flex-col gap-2">
      {connectors.map((connector) => (
        <button
          key={connector.uid}
          onClick={() => connect({ connector })}
          disabled={isPending}
          className="px-4 py-2 bg-blue-500 text-white rounded-lg
                     disabled:opacity-50 hover:bg-blue-600"
        >
          {isPending ? 'Connecting...' : `Connect ${connector.name}`}
        </button>
      ))}
    </div>
  )
}

Handling Connection States

A production app needs to handle various states gracefully:

tsx
// src/hooks/useWalletConnection.ts
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { useEffect, useState } from 'react'
 
export function useWalletConnection() {
  const { address, isConnected, isConnecting, isReconnecting } = useAccount()
  const { connect, connectors, error: connectError } = useConnect()
  const { disconnect } = useDisconnect()
  const [mounted, setMounted] = useState(false)
 
  // Handle hydration mismatch
  useEffect(() => {
    setMounted(true)
  }, [])
 
  return {
    address,
    isConnected: mounted && isConnected,
    isLoading: isConnecting || isReconnecting,
    connect,
    connectors,
    disconnect,
    error: connectError,
  }
}

Reading Blockchain Data

Once connected, you'll want to read data from the blockchain:

Get Wallet Balance

tsx
import { useBalance } from 'wagmi'
 
function WalletBalance() {
  const { address } = useAccount()
  const { data, isLoading } = useBalance({
    address,
  })
 
  if (isLoading) return <span>Loading...</span>
 
  return (
    <span>
      {data?.formatted} {data?.symbol}
    </span>
  )
}

Read Contract Data

tsx
import { useReadContract } from 'wagmi'
import { erc20Abi } from 'viem'
 
function TokenBalance({ tokenAddress }: { tokenAddress: `0x${string}` }) {
  const { address } = useAccount()
 
  const { data: balance } = useReadContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: 'balanceOf',
    args: [address!],
    query: {
      enabled: !!address,
    },
  })
 
  return <span>{balance?.toString()}</span>
}

Writing Transactions

Sending transactions requires careful state management:

tsx
// src/hooks/useContractInteraction.ts
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { parseEther } from 'viem'
 
const contractAbi = [
  {
    name: 'transfer',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'to', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    outputs: [{ type: 'bool' }],
  },
] as const
 
export function useTokenTransfer(contractAddress: `0x${string}`) {
  const {
    writeContract,
    data: hash,
    isPending,
    error
  } = useWriteContract()
 
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash,
  })
 
  const transfer = async (to: `0x${string}`, amount: string) => {
    writeContract({
      address: contractAddress,
      abi: contractAbi,
      functionName: 'transfer',
      args: [to, parseEther(amount)],
    })
  }
 
  return {
    transfer,
    isPending,
    isConfirming,
    isSuccess,
    error,
    hash,
  }
}

Transaction Lifecycle UI

tsx
function TransferButton() {
  const { transfer, isPending, isConfirming, isSuccess, hash } = useTokenTransfer(
    '0x...'
  )
 
  return (
    <div>
      <button
        onClick={() => transfer('0x...', '1.0')}
        disabled={isPending || isConfirming}
      >
        {isPending ? 'Confirm in Wallet...' :
         isConfirming ? 'Processing...' :
         'Send 1 Token'}
      </button>
 
      {isSuccess && (
        <p className="text-green-500">
          Transaction confirmed!{' '}
          <a href={`https://etherscan.io/tx/${hash}`}>View</a>
        </p>
      )}
    </div>
  )
}

Network Handling

Users often need to switch networks. Here's how to handle it:

tsx
// src/components/NetworkSwitcher.tsx
import { useChainId, useSwitchChain } from 'wagmi'
import { mainnet, polygon, sepolia } from 'wagmi/chains'
 
const supportedChains = [mainnet, polygon, sepolia]
 
export function NetworkSwitcher() {
  const chainId = useChainId()
  const { switchChain, isPending } = useSwitchChain()
 
  return (
    <select
      value={chainId}
      onChange={(e) => switchChain({ chainId: Number(e.target.value) })}
      disabled={isPending}
      className="px-3 py-2 border rounded-lg"
    >
      {supportedChains.map((chain) => (
        <option key={chain.id} value={chain.id}>
          {chain.name}
        </option>
      ))}
    </select>
  )
}

Unsupported Network Warning

tsx
import { useChainId } from 'wagmi'
 
const SUPPORTED_CHAIN_IDS = [1, 137, 11155111]
 
function NetworkWarning() {
  const chainId = useChainId()
 
  if (SUPPORTED_CHAIN_IDS.includes(chainId)) {
    return null
  }
 
  return (
    <div className="bg-yellow-100 border-l-4 border-yellow-500 p-4">
      <p className="text-yellow-700">
        Please switch to a supported network (Ethereum, Polygon, or Sepolia)
      </p>
    </div>
  )
}

Best Practices

Error Handling

Always provide clear feedback when things go wrong:

tsx
function ErrorDisplay({ error }: { error: Error | null }) {
  if (!error) return null
 
  // Parse common wallet errors
  let message = 'An error occurred'
 
  if (error.message.includes('User rejected')) {
    message = 'Transaction was cancelled'
  } else if (error.message.includes('insufficient funds')) {
    message = 'Insufficient funds for this transaction'
  } else if (error.message.includes('nonce')) {
    message = 'Transaction nonce error. Please reset your wallet.'
  }
 
  return (
    <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
      {message}
    </div>
  )
}

Loading States

Keep users informed during async operations:

tsx
function TransactionStatus({
  isPending,
  isConfirming
}: {
  isPending: boolean
  isConfirming: boolean
}) {
  if (isPending) {
    return (
      <div className="flex items-center gap-2">
        <Spinner />
        <span>Please confirm in your wallet...</span>
      </div>
    )
  }
 
  if (isConfirming) {
    return (
      <div className="flex items-center gap-2">
        <Spinner />
        <span>Waiting for confirmation...</span>
      </div>
    )
  }
 
  return null
}

For mobile users, ensure wallet apps can be opened:

tsx
import { useConnect } from 'wagmi'
 
function MobileConnectButton() {
  const { connect, connectors } = useConnect()
 
  const walletConnect = connectors.find(c => c.id === 'walletConnect')
 
  return (
    <button
      onClick={() => walletConnect && connect({ connector: walletConnect })}
      className="w-full px-4 py-3 bg-blue-500 text-white rounded-lg"
    >
      Connect Mobile Wallet
    </button>
  )
}

Production Checklist

Before deploying your dApp, ensure you've covered:

  • Environment variables - Never expose private keys or sensitive data
  • Error boundaries - Catch and handle React errors gracefully
  • Network validation - Always check if user is on correct network
  • Transaction confirmation - Wait for adequate block confirmations
  • Reconnection logic - Handle page refreshes and session persistence
  • Mobile testing - Test with actual mobile wallets
  • Loading states - Show feedback for all async operations
  • Gas estimation - Provide gas estimates before transactions

Conclusion

Wallet integration is the gateway to your Web3 application. By using Wagmi's React hooks, you get a type-safe, well-tested foundation that handles the complexities of blockchain interaction.

The patterns shown here will scale from simple dApps to complex DeFi protocols. Start with the basics, then layer on features like multi-chain support and advanced transaction management as your application grows.

In future posts, I'll cover topics like gas optimization and building your own DEX. Stay tuned!

Resources

  • Wagmi Documentation
  • Viem Documentation
  • WalletConnect Cloud (for project IDs)
  • Rainbow Kit (pre-built UI components)
N

Nawab Khairuzzaman

Full-Stack Web & Blockchain Developer with 6+ years of experience building scalable applications.

Comments

Related Posts