一、wagmi和viem是什么
1.1 为什么要用wagmi
直接用ethers.js或viem写以太坊交互也能工作,但涉及到React状态管理、缓存、自动重连、错误处理这些「脚手架」代码时,工作量就上来了。wagmi把这些都封装成React Hooks,让开发者专注于业务逻辑。
wagmi的核心价值:
- 开箱即用的React Hooks:连接钱包、读取数据、发送交易都有现成方案
- 自动状态管理:连接状态、链ID、余额变化自动同步到组件
- 缓存与去重:基于TanStack Query,不会重复发送相同的请求
- TypeScript优先:类型提示完善,开发时就能发现错误
1.2 viem的角色
viem是底层库,负责与以太坊节点的通信。它比ethers.js更轻量、更快,提供了完整的以太坊JSON-RPC API封装。wagmi依赖viem作为传输层,同时提供React层面的抽象。
两者的关系:viem做网络和编码工作,wagmi做React集成工作。

二、项目初始化
2.1 创建React项目
我习惯用Vite创建项目,比Create React App快很多:
bash
npm create vite@latest my-web3-dapp -- --template react-ts
cd my-web3-dapp
npm install
2.2 安装依赖
bash
# 核心依赖
npm install wagmi viem @tanstack/react-query
# UI组件库(可选,推荐RainbowKit)
npm install @rainbow-me/rainbowkit
npm install @tanstack/react-query
# 用于样式(Vite默认没有CSS方案)
npm install tailwindcss postcss autoprefixer
npx tailwindcss init -p
2.3 基础配置
创建wagmi配置文件:
typescript
// src/wagmi.config.ts
import { http, createConfig } from 'wagmi'
import { mainnet, sepolia } from 'wagmi/chains'
import { injected, walletConnect } from 'wagmi/connectors'
// WalletConnect项目ID(https://cloud.walletconnect.com/ 注册)
const projectId = import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID
export const config = createConfig({
chains: [mainnet, sepolia],
connectors: [
injected(), // 浏览器插件钱包(MetaMask、Coinbase Wallet等)
walletConnect({
projectId,
metadata: {
name: 'My Web3 DApp',
description: 'A sample Web3 application',
url: window.location.origin,
icons: ['https://example.com/icon.png']
}
}),
],
transports: {
[mainnet.id]: http(), // 使用公共RPC,生产环境建议用自己的节点
[sepolia.id]: http(),
},
})
2.4 在main.tsx中配置Provider
typescript
// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import App from './App'
import { config } from './wagmi.config'
import '@rainbow-me/rainbowkit/styles.css' // RainbowKit样式
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
<App />
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
</React.StrictMode>,
)
三、钱包连接实战
3.1 连接按钮组件
使用RainbowKit可以快速获得美观的连接按钮:
typescript
// src/components/ConnectButton.tsx
import { ConnectButton } from '@rainbow-me/rainbowkit'
export function WalletConnect() {
return (
<ConnectButton
showBalance={{
smallScreen: false,
largeScreen: true,
}}
chainStatus="icon"
accountStatus={{
smallScreen: 'avatar',
largeScreen: 'full',
}}
/>
)
}
3.2 自定义连接逻辑
如果不使用RainbowKit,可以自己实现:
typescript
// src/components/CustomConnect.tsx
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { injected } from 'wagmi/connectors'
export function CustomConnect() {
const { address, isConnected } = useAccount()
const { connect } = useConnect()
const { disconnect } = useDisconnect()
if (isConnected) {
return (
<div>
<p>Connected: {address?.slice(0, 6)}...{address?.slice(-4)}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
)
}
return (
<button onClick={() => connect({ connector: injected() })}>
Connect Wallet
</button>
)
}
四、读取区块链数据
4.1 读取原生币余额
typescript
import { useBalance, useAccount } from 'wagmi'
function ETHBalance() {
const { address, isConnected } = useAccount()
const { data, isError, isLoading } = useBalance({
address,
query: {
enabled: !!isConnected,
}
})
if (!isConnected) return <p>Please connect your wallet</p>
if (isLoading) return <p>Loading...</p>
if (isError) return <p>Error fetching balance</p>
return (
<div>
<p>Balance: {data?.formatted} {data?.symbol}</p>
<p>In USD: ${(Number(data?.formatted) * 3500).toFixed(2)}</p>
</div>
)
}
4.2 读取ERC20代币余额
需要自己构建读取合约的请求:
typescript
import { useContractRead, useAccount } from 'wagmi'
import { erc20Abi } from 'viem'
// USDC地址(主网)
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
function USDCBalance() {
const { address, isConnected } = useAccount()
const { data: balance } = useContractRead({
address: USDC_ADDRESS,
abi: erc20Abi,
functionName: 'balanceOf',
args: [address!],
query: {
enabled: !!address,
}
})
if (!isConnected) return <p>Please connect your wallet</p>
// USDC是6位精度
const formattedBalance = Number(balance) / 10 ** 6
return <p>USDC Balance: {formattedBalance.toFixed(2)}</p>
}
4.3 批量读取多个合约
useContractReads可以一次读取多个合约的状态,减少网络请求:
typescript
import { useContractReads, useAccount } from 'wagmi'
import { erc20Abi } from 'viem'
const TOKENS = [
{ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', symbol: 'USDC', decimals: 6 },
{ address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'WETH', decimals: 18 },
]
function TokenBalances() {
const { address, isConnected } = useAccount()
const { data, isLoading } = useContractReads({
contracts: TOKENS.map((token) => ({
address: token.address as `0x${string}`,
abi: erc20Abi,
functionName: 'balanceOf',
args: [address!],
})),
query: { enabled: !!address }
})
if (!isConnected) return <p>Connect wallet</p>
if (isLoading) return <p>Loading...</p>
return (
<div>
{TOKENS.map((token, index) => {
const balance = data?.[index]?.result
const formatted = balance ? Number(balance) / 10 ** token.decimals : 0
return (
<p key={token.symbol}>
{token.symbol}: {formatted.toFixed(4)}
</p>
)
})}
</div>
)
}
五、发送交易
5.1 发送ETH
typescript
import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi'
import { parseEther } from 'viem'
function SendETH() {
const { sendTransaction, data: hash, isPending, error } = useSendTransaction()
const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({
hash,
})
async function handleSend(to: string, amount: string) {
sendTransaction({
to: to as `0x${string}`,
value: parseEther(amount), // 字符串转成Wei
})
}
return (
<div>
<button
onClick={() => handleSend('0x...', '0.01')}
disabled={isPending}
>
{isPending ? 'Confirming...' : 'Send 0.01 ETH'}
</button>
{error && <p>Error: {error.message}</p>}
{isConfirming && <p>Waiting for confirmation...</p>}
{isConfirmed && <p>Transaction confirmed!</p>}
</div>
)
}
5.2 调用合约函数
以ERC20的transfer为例:
typescript
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { erc20Abi } from 'viem'
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
function TransferUSDC() {
const {
writeContract,
data: hash,
isPending,
error
} = useWriteContract()
const { isLoading: isConfirming, isSuccess: isConfirmed } =
useWaitForTransactionReceipt({ hash })
function handleTransfer(to: string, amount: string) {
// amount是USDC的原始精度(6位小数)
writeContract({
address: USDC_ADDRESS,
abi: erc20Abi,
functionName: 'transfer',
args: [
to as `0x${string}`,
BigInt(Math.floor(Number(amount) * 10 ** 6))
],
})
}
return (
<div>
<button
onClick={() => handleTransfer('0x...', '100')}
disabled={isPending}
>
{isPending ? 'Sign in wallet...' : 'Transfer 100 USDC'}
</button>
{isConfirming && <p>Transaction submitting...</p>}
{isConfirmed && <p>Transfer complete!</p>}
{error && <p>Error: {error.shortMessage || error.message}</p>}
</div>
)
}
六、实战案例:DeFi代币兑换界面
6.1 组件结构
一个简单的代币兑换界面需要:
- 代币选择器
- 金额输入
- 汇率显示
- Swap按钮
- 交易状态显示
typescript
// src/components/Swap.tsx
import { useState } from 'react'
import { useAccount, useBalance } from 'wagmi'
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
// 简化示例:直接调用Uniswap V2 Router
const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
export function Swap() {
const { address, isConnected } = useAccount()
// 代币信息
const [tokenIn, setTokenIn] = useState('ETH')
const [tokenOut, setTokenOut] = useState('USDC')
const [amountIn, setAmountIn] = useState('')
const { writeContract, data: hash, isPending } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })
// 读取ETH余额
const { data: ethBalance } = useBalance({ address })
// 处理Swap
async function handleSwap() {
if (!amountIn) return
const amountInWei = parseEther(amountIn)
writeContract({
address: ROUTER_ADDRESS,
abi: UNISWAP_V2_ROUTER_ABI,
functionName: 'swapExactETHForTokens',
args: [
BigInt(0), // 最小输出金额,生产环境应该设合理值
[WETH_ADDRESS, USDC_ADDRESS], // 交易路径
address!,
BigInt(Math.floor(Date.now() / 1000) + 60 * 20) // 20分钟过期
],
value: amountInWei,
})
}
if (!isConnected) {
return <p>Please connect your wallet</p>
}
return (
<div className="swap-container">
<div className="input-group">
<label>You Pay</label>
<input
type="number"
value={amountIn}
onChange={(e) => setAmountIn(e.target.value)}
placeholder="0.0"
/>
<span>{tokenIn}</span>
<p>Balance: {ethBalance?.formatted}</p>
</div>
<button onClick={handleSwap} disabled={isPending || !amountIn}>
{isPending ? 'Sign in wallet...' : 'Swap'}
</button>
{isSuccess && <p>Swap successful!</p>}
</div>
)
}
七、常见问题与解决方案
7.1 用户拒绝签名
用户可能拒绝交易或者根本就没装钱包:
typescript
const { writeContract, error } = useWriteContract()
// error.shortMessage 通常包含有用的信息
// "User rejected the request" = 用户拒绝
// "insufficient funds" = 余额不足
7.2 链不匹配
用户连接的链与DApp不匹配时,需要提示切换:
typescript
import { useChainId, useSwitchChain } from 'wagmi'
function NetworkWarning() {
const chainId = useChainId()
const { switchChain } = useSwitchChain()
if (chainId !== sepolia.id) {
return (
<div className="warning">
<p>Please switch to Sepolia testnet</p>
<button onClick={() => switchChain({ chainId: sepolia.id })}>
Switch Network
</button>
</div>
)
}
}
7.3 交易Pending处理
用户发起交易后,有时候会Pending很久:
typescript
import { useWaitForTransactionReceipt } from 'wagmi'
const { isLoading: isPending } = useWaitForTransactionReceipt({
hash,
// 超时设置
timeout: 1000 * 60 * 5, // 5分钟超时
})
if (isPending) {
return <p>Transaction pending for a while... Please check your wallet.</p>
}
八、生产环境注意事项
8.1 使用自己的RPC节点
公共RPC有速率限制,生产环境应该用Alchemy或Infura:
typescript
import { http } from 'wagmi'
import { mainnet } from 'wagmi/chains'
const alchemyKey = import.meta.env.VITE_ALCHEMY_API_KEY
export const config = createConfig({
chains: [mainnet],
transports: {
[mainnet.id]: http(`https://eth-mainnet.g.alchemy.com/v2/${alchemyKey}`),
},
})
8.2 其他优化建议
注意缓存策略和错误边界。TanStack Query的缓存配置要适合区块链数据的实时性,错误边界能防止单个组件崩溃影响整个应用。
结语
wagmi和viem让Web3前端开发变得异常流畅。你不需要成为以太坊专家,只需要理解几个核心概念——连接钱包、读取数据、发送交易——就能构建出体验良好的DApp。
我建议的学习路径是:
- 先跑通官方的示例项目
- 学会连接钱包和读取余额
- 尝试发送一笔简单的ETH转账
- 挑战一个完整的合约调用(比如ERC20转账)
- 最后实现一个简单的DeFi交互界面
Web3前端开发的门槛已经很低了,缺的只是更多有热情加入的开发者。
相关阅读:
