Connect Wallets in React: A Guide.

Okay, look. Most wallet connection guides out there? They throw a million code snippets at you without explaining why your app crashes when MetaMask switches networks mid session. Or they skip the part where users rage quit because the connect button spins forever on mobile. In my experience, that's the killer-ignoring real world mess like chain mismatches or browser permissions. You'll see that fixed here. No fluff. Just what works.

Why does this matter? Because one bad connection flow and your users bounce. I've built like 20 dApps. Trust me, smooth wallet stuff keeps 'em hooked.

Pick Your Weapon: Which Wallet Library?

Don't overthink it at first. Start simple. If you're on Ethereum or EVM chains, thirdweb or plain Web3.js with MetaMask is dead easy. Solana? Wallet Adapter. Want everything? RainbowKit or Wagmi.

  • Thirdweb: Gasless smart wallets. Create new ones on the fly. Perfect for no ETH newbies.
  • Web3.js + MetaMask: Old school but bulletproof. Gas fees ~20k on ETH mainnet.
  • @solana/wallet adapter react: Solana only. Handles Phantom, Backpack, all that.
  • Wagmi + connectors: Fancy. Multi chain. But steeper curve.

The thing is, pick based on your chain. Mixing? Pain. I usually go thirdweb for quick prototypes-handles smart wallets that pay gas for users.

Thirdweb Way: Smart Wallets That Don't Suck

So, smart wallets. Users don't need ETH upfront. App sponsors gas. Game changer for onboarding.

First, spin up a Vite React app. I do this daily:

  1. npx thirdweb create app --vite --ts. Boom, preconfigured SDK.
  2. Grab your client ID from thirdweb dashboard. Free tier's fine.
  3. Deploy a smart wallet factory. Go to thirdweb.com/contracts, pick Account Factory (Beta), hit Deploy Now. Two txns, ~0.01 ETH on Sepolia.
  4. Copy that factory address. That's your golden ticket.

Now main.tsx. Here's the config I always use:

import { ThirdwebProvider } from "@thirdweb dev/react";
import { smartWallet } from "@thirdweb dev/wallets";
import { localWallet } from "@thirdweb dev/wallets";
import { Mumbai } from "@thirdweb dev/chains"; const activeChain = "mumbai"; // Testnet. Swap for mainnet later. export const smartWalletConfig = smartWallet({ factoryAddress: "0xYourFactoryAddressHere", // From step 3 gasless: true, // App pays gas. ~0.000005 ETH equivalent. personalWallets: [localWallet()],
}); function App() { return ( <ThirdwebProvider clientId={import.meta.env.VITETEMPLATECLIENT_ID} activeChain={activeChain} supportedWallets={[smartWalletConfig]} > <ConnectWallet /> // Pre built button. Magic. </ThirdwebProvider> );
}

Run npm run dev. Click ConnectWallet. Popup asks for password or import. Creates a smart account instantly. Address shows up. Done.

But wait. Custom UI? Yeah, most guides stop here. Users want branded buttons.

Custom Connect Hook

Create components/Connect.tsx. Steal this-I use it everywhere:

import { useAddress, useConnect } from "@thirdweb dev/react";
import { LocalWallet } from "@thirdweb dev/wallets";
import { Mumbai } from "@thirdweb dev/chains";
import { smartWalletConfig } from "./main";
import { useState } from "react"; export const ConnectComponent = () => { const connect = useConnect(); const address = useAddress(); const [password, setPassword] = useState(""); const loadLocalWalletAndConnect = async () => { if (!password) return alert("Password, dude?"); try { const personalWallet = new LocalWallet({ chain: Mumbai }); await personalWallet.loadOrCreate({ strategy: "encryptedJson", password, }); await connect(smartWalletConfig, { personalWallet }); } catch (e) { alert((e as any).message); } }; if (address) { return ( <h3> Connected: <a href={https://thirdweb.com/mumbai/${address}} target="_blank">{address}</a> </h3> ); } return ( <> <input type="password" placeholder="Enter Password" onChange={(e) => setPassword(e.target.value)} /> <button onClick={loadLocalWalletAndConnect}>Log In</button> </> );
};

Drop it in App.tsx. Users enter password. Smart wallet deploys. Gasless txns ready. What's next? Listen for disconnects.

Hooks like useDisconnect(). Call on logout. Handles session cleanup automatically.

MetaMask + Web3.js: The Classic That Still Wins

Okay, switch gears. No SDK bloat. Pure Web3.js. Gas fees visible: ~21k for connects on ETH.

Setup? Dead simple.

  1. npx create react app metamask app
  2. npm i web3 @metamask/detect provider

App.js. This hook I wrote years ago-still use it:

import { useState, useEffect } from 'react';
import Web3 from 'web3';
import detectEthereumProvider from '@metamask/detect provider'; function App() { const [account, setAccount] = useState(''); const [web3, setWeb3] = useState(null); const connectWallet = async () => { const provider = await detectEthereumProvider(); if (!provider) return alert("Install MetaMask, bro."); if (provider !== window.ethereum) return alert("Wrong provider."); await provider.request({ method: 'eth_requestAccounts' }); const web3Instance = new Web3(provider); setWeb3(web3Instance); const accounts = await web3Instance.eth.getAccounts(); setAccount(accounts); // Listen for changes provider.on('accountsChanged', (accs) => { setAccount(accs); }); provider.on('chainChanged', () => window.location.reload()); }; return ( <div> {!account ? ( <button onClick={connectWallet}>Connect MetaMask</button> ) : ( <p>Hey {account.slice(0,6)}..{account.slice(-4)}</p> )} </div> );
}

Click. MetaMask pops. Approve. Account shows truncated. Sweet. But issues? Chain wrong? Add chainId check:

const chainId = await web3.eth.getChainId();
if (chainId !== 1) { // Mainnet await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0x1' }], });
}

Solves 80% of "wrong network" errors. Users hate those.

Solana: Phantom and Friends

Solana's different. Fees tiny: ~0.000005 SOL per txn. Wallet Adapter rules here.

New Next.js? Or React.

  1. npm i @solana/web3.js @solana/wallet adapter base @solana/wallet adapter react @solana/wallet adapter react ui @solana/wallet adapter wallets
  2. Providers wrapper. Make SolanaProvider.tsx:
"use client";
import { ConnectionProvider, WalletProvider } from '@solana/wallet adapter react';
import { WalletAdapterNetwork } from '@solana/wallet adapter base';
import { WalletModalProvider } from '@solana/wallet adapter react ui';
import { PhantomWalletAdapter } from '@solana/wallet adapter wallets';
import { clusterApiUrl } from '@solana/web3.js'; const network = WalletAdapterNetwork.Devnet;
const endpoint = clusterApiUrl(network);
const wallets = [new PhantomWalletAdapter()]; export function SolanaProvider({ children }) { return ( <ConnectionProvider endpoint={endpoint}> <WalletProvider wallets={wallets} autoConnect> <WalletModalProvider>{children}</WalletModalProvider> </WalletProvider> </ConnectionProvider> );
}

Wrap your app root. Add <WalletMultiButton /> from adapter ui. Boom. Phantom connects. Devnet SOL faucet fills it free.

Custom? useWallet() hook. Gives publicKey, signMessage, etc. Super clean.

One Button Rules Them All: Universal Connect

MethodProsConsGas/Fees
Thirdweb SmartGasless, new walletsFactory deploy needed0 upfront
MetaMask Web3Simple, no depsUser pays gas~21k ETH
Solana AdapterFast, cheapSolana only0.000005 SOL
WagmiMulti chainLearning curveChain dep.

Want one button for MetaMask + WalletConnect? Hook city.

// hooks/useUniversalConnect.js
import { useMetaMask } from './useMetaMask'; // Your MM hook
import { useWalletConnect } from './useWalletConnect'; // WC hook export const UniversalConnectButton = () => { const { account: mmAccount, connect: mmConnect } = useMetaMask(); const { account: wcAccount, connect: wcConnect } = useWalletConnect(); const isConnected = mmAccount || wcAccount; return isConnected ? ( <p>Connected: {mmAccount || wcAccount}</p> ) : ( <> <button onClick={mmConnect}>MetaMask</button> <button onClick={wcConnect}>WalletConnect</button> </> );
};

WalletConnect needs Infura ID for RPC. Free tier: 100k reqs/day. QR pops on mobile. Scans perfect.

Troubleshooting: Fix the Crashes

Users connected. Great. Now it breaks.

No provider? MetaMask not installed. Check window.ethereum. Alert nicely: "Get MetaMask first."

Chain mismatch? Always check chainId post connect. Switch auto. But warn: "Switching to mainnet?"

Mobile hell? WalletConnect QR. Deep links back. Test WiFi off-reconnects in 3s.

Loading forever? Add timeouts. 10s max. Show "Still connecting.." then error.

  • Show loading spinner. Always.
  • Error messages: "Failed: Check MetaMask approval."
  • Auto reconnect on refresh. useEffect checks localStorage.
  • Multiple wallets? List 'em. Don't force one.

In my experience, 90% issues are permissions or networks. Log everything to console first.

Edge Cases Table

ProblemFix
App killed mid approvalReconnect on reopen
WiFi dropsWebSocket retry + notify
Wrong chainwallet_switchEthereumChain
No gasSmart wallets or warn

Production Polish

Don't ship raw. Add this.

Context for state. Wrap providers:

const WalletContext = createContext(); export const WalletProvider = ({ children }) => { const [isConnected, setIsConnected] = useState(false); // Your state here return ( <WalletContext.Provider value={{ isConnected, connectWallet }}> {children} </WalletContext.Provider> );
};

useContext in components. No prop drilling. Scales huge.

Tests? Jest. Mock window.ethereum.

test('connects MetaMask', async () => { window.ethereum = { request: jest.fn() }; // Assert
});

Eager connect: On mount, check if already approved. Saves re clicks.

Security? Never store private keys client side. Wallets handle that. Encrypt local ones with password.

Honesty time: I've seen dApps leak sessions. useEffect cleanup listeners. Always.