MetaMask Integration in React
Everything you need to know about connecting wallets to your React application. From basic setup to handling edge cases like network switching and account changes.
Introduction
Wallet connection is the entry point for most Web3 applications. MetaMask remains the dominant choice, with over 30 million monthly active users. Getting this integration right is crucial for user onboarding.
In this guide, we'll cover:
- Setting up a React project with wagmi and viem
- Creating a reusable wallet connection button
- Handling network switching gracefully
- Managing account changes and disconnections
- Best practices for production applications
Project Setup
We'll use wagmi v2 with viem for Ethereum interactions. This combination provides type-safe hooks and excellent developer experience.
npm install wagmi viem @tanstack/react-query
# For TypeScript, also install types
npm install -D @types/nodeConfiguring the Wagmi Provider
First, we need to set up the WagmiProvider and QueryClientProvider. These should wrap your entire application.
// app/providers.tsx
'use client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { config } from '@/lib/wagmi'
const queryClient = new QueryClient()
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
)
}Wagmi Config
Configure the chains and transports your app supports. Always include mainnet for broad compatibility.
// lib/wagmi.ts
import { http, createConfig } from 'wagmi'
import { mainnet, polygon, optimism } from 'wagmi/chains'
export const config = createConfig({
chains: [mainnet, polygon, optimism],
transports: {
[mainnet.id]: http(),
[polygon.id]: http(),
[optimism.id]: http(),
},
})The Connect Button Component
Here's a production-ready wallet button that handles loading, errors, and disconnected states elegantly.
'use client'
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { useState } from 'react'
export function WalletButton() {
const [showDropdown, setShowDropdown] = useState(false)
const { address, isConnected, isConnecting } = useAccount()
const { connect, connectors, isPending, error } = useConnect()
const { disconnect } = useDisconnect()
if (isConnected && address) {
return (
<div className="relative">
<button
onClick={() => setShowDropdown(!showDropdown)}
className="px-4 py-2 bg-white/10 rounded-full text-sm"
>
{address.slice(0, 6)}...{address.slice(-4)}
</button>
{showDropdown && (
<div className="absolute top-full right-0 mt-2 bg-black border border-white/10 rounded-lg p-2">
<button
onClick={() => disconnect()}
className="w-full text-left px-4 py-2 hover:bg-white/5 rounded text-sm"
>
Disconnect
</button>
</div>
)}
</div>
)
}
return (
<div className="relative">
<button
onClick={() => setShowDropdown(!showDropdown)}
disabled={isConnecting}
className="px-6 py-3 bg-white text-black rounded-full font-medium disabled:opacity-50"
>
{isConnecting ? 'Connecting...' : 'Connect Wallet'}
</button>
{showDropdown && (
<div className="absolute top-full right-0 mt-2 bg-black border border-white/10 rounded-lg p-2 min-w-[200px]">
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => connect({ connector })}
disabled={isPending}
className="w-full text-left px-4 py-2 hover:bg-white/5 rounded text-sm disabled:opacity-50"
>
{connector.name}
</button>
))}
{error && (
<p className="px-4 py-2 text-red-500 text-xs">
{error.message}
</p>
)}
</div>
)}
</div>
)
}Handling Network Switching
Users often need to switch networks. Always prompt them to add or switch when they try to use a feature on an unsupported chain.
import { useSwitchChain } from 'wagmi'
export function NetworkSwitcher({ targetChainId }: { targetChainId: number }) {
const { switchChain, isPending, error } = useSwitchChain()
return (
<button
onClick={() => switchChain({ chainId: targetChainId })}
disabled={isPending}
className="px-4 py-2 bg-orange-500/20 text-orange-400 rounded-full text-sm"
>
{isPending ? 'Switching...' : 'Switch to Polygon'}
</button>
)
}Listening for Account Changes
MetaMask can disconnect unexpectedly. Use the useAccount hook's listeners to handle these events gracefully.
import { useAccount, useDisconnect } from 'wagmi'
export function AccountListener() {
const { address } = useAccount()
const { disconnect } = useDisconnect()
// Handle account changes
useEffect(() => {
if (!address) {
// Clear app state when disconnected externally
localStorage.removeItem('user_session')
}
}, [address])
// Handle disconnection from MetaMask popup
window.ethereum?.on('accountsChanged', (accounts: string[]) => {
if (accounts.length === 0) {
disconnect()
}
})
}Production Considerations
- Always show loading states — Wallet connections take time
- Handle rejection gracefully — Users can cancel at any point
- Persist connection state — Use localStorage to remember recent connections
- Support multiple wallets — Don't assume MetaMask is installed
- Test on mobile — WalletConnect or Coinbase Wallet may be preferred
Conclusion
Wallet integration is more than just connecting. It requires handling edge cases, providing clear feedback, and respecting user experience at every step. The patterns above have served us well across dozens of Web3 projects.
Need help integrating wallets into your product? We're experienced with wallet connections across React, Vue, and vanilla JS applications.
Building a Web3 product? Let's talk →