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.
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.
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.
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:
npx thirdweb create app --vite --ts. Boom, preconfigured SDK.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.
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.
Okay, switch gears. No SDK bloat. Pure Web3.js. Gas fees visible: ~21k for connects on ETH.
Setup? Dead simple.
npx create react app metamask appnpm i web3 @metamask/detect providerApp.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's different. Fees tiny: ~0.000005 SOL per txn. Wallet Adapter rules here.
New Next.js? Or React.
npm i @solana/web3.js @solana/wallet adapter base @solana/wallet adapter react @solana/wallet adapter react ui @solana/wallet adapter wallets"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.
| Method | Pros | Cons | Gas/Fees |
|---|---|---|---|
| Thirdweb Smart | Gasless, new wallets | Factory deploy needed | 0 upfront |
| MetaMask Web3 | Simple, no deps | User pays gas | ~21k ETH |
| Solana Adapter | Fast, cheap | Solana only | 0.000005 SOL |
| Wagmi | Multi chain | Learning curve | Chain 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.
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.
In my experience, 90% issues are permissions or networks. Log everything to console first.
| Problem | Fix |
|---|---|
| App killed mid approval | Reconnect on reopen |
| WiFi drops | WebSocket retry + notify |
| Wrong chain | wallet_switchEthereumChain |
| No gas | Smart wallets or warn |
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.