Wagmi 与 Web3 React 集成
问题
Wagmi 是什么?它如何帮助 React 应用与以太坊交互?核心 Hooks 有哪些?
Wagmi 是什么? Wagmi 是 React Hooks for Ethereum,底层是 Viem + TanStack Query:
- Viem 负责链上调用(RPC 请求、合约调用、交易发送)。
- TanStack Query 负责缓存、请求去重、后台刷新、轮询。
- Wagmi 把两者包装成「声明式」Hooks,写 DApp 手感接近传统 React。
核心 Hooks 有哪些? 记住「连接、读、写、听」四类:
- 连接:
useAccount(当前账户/连接状态)、useConnect(调起钱包)、useDisconnect、useSwitchChain(切链)。 - 读:
useBalance(余额)、useReadContract(合约 view 函数,自动堆调用拼成 multicall)、useReadContracts批量读。 - 写:
useWriteContract(调交易,返 hash)、useWaitForTransactionReceipt(等确认),两个配合完成「发起→上链」闭环。 - 听:
useWatchContractEvent订阅合约事件、useBlockNumber({ watch: true })听区块。
怎么用?
- 项目初始化:
createConfig({ chains, transports, connectors })+WagmiProvider+QueryClientProvider。 - 调用代码全部是 Hooks,合约变量变了会自动重新获取、会随账号/链切换重新请求。
- 配合 RainbowKit / ConnectKit 可以三行代码拥有「连钱包」弹窗,包含手机、硬件、WalletConnect。
答案
Wagmi 是一套 React Hooks for Ethereum,基于 Viem 和 TanStack Query 构建,提供了类型安全、可组合的 Hooks,让 React 应用能够优雅地完成钱包连接、余额查询、合约交互等 Web3 操作。
Wagmi 将底层的 JSON-RPC 调用和钱包交互抽象为声明式的 React Hooks,同时利用 TanStack Query 的缓存和状态管理能力,让 DApp 开发体验接近传统 Web 应用。
架构设计
Wagmi 采用 Config → Provider → Hooks 三层架构:
| 层级 | 职责 | 核心 API |
|---|---|---|
| 配置层 | 定义链、传输、连接器 | createConfig |
| Provider 层 | 注入配置和查询客户端 | WagmiProvider + QueryClientProvider |
| Hooks 层 | 声明式读写区块链 | useAccount、useReadContract 等 |
配置
createConfig
createConfig 是 Wagmi 的入口,定义了应用支持的链、传输方式和钱包连接器:
import { createConfig, http } from 'wagmi'
import { mainnet, sepolia, polygon } from 'wagmi/chains'
import { injected, walletConnect, coinbaseWallet } from 'wagmi/connectors'
export const config = createConfig({
// 支持的链
chains: [mainnet, sepolia, polygon],
// 每条链的 RPC 传输方式
transports: {
[mainnet.id]: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
[sepolia.id]: http('https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY'),
[polygon.id]: http(), // 使用默认公共 RPC
},
// 钱包连接器
connectors: [
injected(), // MetaMask 等浏览器钱包
walletConnect({ projectId: 'YOUR_PROJECT_ID' }), // WalletConnect v2
coinbaseWallet({ appName: 'My DApp' }), // Coinbase Wallet
],
})
Provider 配置
在应用根组件中包裹 WagmiProvider 和 QueryClientProvider:
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { config } from './config'
const queryClient = new QueryClient()
export default function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
</WagmiProvider>
)
}
WagmiProvider 依赖 QueryClientProvider,缺少任何一个都会导致 Hooks 报错。TanStack Query 提供了缓存、重试、后台刷新等能力。
核心 Hooks
useAccount — 连接状态
获取当前连接的账户信息:
import { useAccount } from 'wagmi'
function Profile() {
const {
address, // 钱包地址,如 '0x...'
isConnected, // 是否已连接
isConnecting, // 是否正在连接
chain, // 当前链信息
connector, // 当前使用的连接器
status, // 'connected' | 'connecting' | 'disconnected' | 'reconnecting'
} = useAccount()
if (!isConnected) return <div>请连接钱包</div>
return (
<div>
<p>地址:{address}</p>
<p>链:{chain?.name}</p>
</div>
)
}
useConnect / useDisconnect — 连接与断开
import { useConnect, useDisconnect } from 'wagmi'
function WalletButtons() {
// useConnect 返回可用的连接器列表和连接方法
const { connect, connectors, isPending, error } = useConnect()
const { disconnect } = useDisconnect()
return (
<div>
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => connect({ connector })}
disabled={isPending}
>
{connector.name}
</button>
))}
<button onClick={() => disconnect()}>断开连接</button>
{error && <p>错误:{error.message}</p>}
</div>
)
}
useBalance — 余额查询
import { useBalance } from 'wagmi'
function Balance() {
// 查询原生代币(ETH)余额
const { data: ethBalance } = useBalance({
address: '0x...',
})
// 查询 ERC-20 代币余额,传入 token 合约地址
const { data: usdtBalance } = useBalance({
address: '0x...',
token: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
})
return (
<div>
<p>ETH: {ethBalance?.formatted} {ethBalance?.symbol}</p>
<p>USDT: {usdtBalance?.formatted} {usdtBalance?.symbol}</p>
</div>
)
}
useReadContract — 读取合约
调用合约的 view / pure 函数,不消耗 Gas:
import { useReadContract } from 'wagmi'
import { erc20Abi } from 'viem'
function TokenInfo() {
const { data: totalSupply } = useReadContract({
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
abi: erc20Abi, // Viem 内置的 ERC-20 ABI
functionName: 'totalSupply',
})
// 带参数的读取
const { data: allowance } = useReadContract({
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
abi: erc20Abi,
functionName: 'allowance',
args: ['0xOwnerAddress...', '0xSpenderAddress...'],
})
return <p>总供应量:{totalSupply?.toString()}</p>
}
useWriteContract — 写入合约
调用合约的状态修改函数,会发起交易并消耗 Gas:
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { erc20Abi, parseUnits } from 'viem'
function ApproveButton() {
const {
writeContract,
data: hash, // 交易哈希
isPending, // 等待用户确认
error,
} = useWriteContract()
// 等待交易被区块链确认
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash,
})
function handleApprove() {
writeContract({
address: '0xTokenAddress...',
abi: erc20Abi,
functionName: 'approve',
args: [
'0xSpenderAddress...',
parseUnits('1000', 18), // 授权 1000 个代币
],
})
}
return (
<div>
<button onClick={handleApprove} disabled={isPending}>
{isPending ? '确认中...' : 'Approve'}
</button>
{isConfirming && <p>交易确认中...</p>}
{isSuccess && <p>授权成功!</p>}
{error && <p>错误:{error.message}</p>}
</div>
)
}
useWaitForTransactionReceipt — 等待交易确认
该 Hook 接收一个交易哈希,在交易被打包进区块后返回收据:
const { data: receipt, isLoading, isSuccess, isError } =
useWaitForTransactionReceipt({
hash: '0x...', // 来自 useWriteContract 或 useSendTransaction 的哈希
confirmations: 2, // 等待 2 个区块确认(默认 1)
})
useSignMessage / useSignTypedData — 签名
import { useSignMessage } from 'wagmi'
function SignButton() {
const { signMessage, data: signature, isPending } = useSignMessage()
return (
<button onClick={() => signMessage({ message: 'Hello Web3!' })}>
{isPending ? '签名中...' : '签名消息'}
</button>
)
}
useSignTypedData 用于 EIP-712 结构化数据签名,常见于 Permit、Gasless 交易等场景。
useWatchContractEvent — 监听合约事件
import { useWatchContractEvent } from 'wagmi'
import { erc20Abi } from 'viem'
function TransferWatcher() {
useWatchContractEvent({
address: '0xTokenAddress...',
abi: erc20Abi,
eventName: 'Transfer',
onLogs(logs) {
logs.forEach((log) => {
console.log('From:', log.args.from)
console.log('To:', log.args.to)
console.log('Value:', log.args.value)
})
},
})
return <p>正在监听 Transfer 事件...</p>
}
连接器(Connector)
Wagmi 通过连接器(Connector)抽象不同钱包的接入方式:
| 连接器 | 说明 | 使用场景 |
|---|---|---|
injected() | 注入式钱包(window.ethereum) | MetaMask、OKX Wallet 等浏览器扩展 |
walletConnect() | WalletConnect v2 协议 | 移动端钱包扫码连接 |
coinbaseWallet() | Coinbase Wallet SDK | Coinbase 用户 |
safe() | Gnosis Safe | 多签钱包 |
metaMask() | MetaMask SDK | MetaMask 专用增强功能 |
injected() 连接器会自动检测浏览器中安装的钱包。如果用户同时安装了 MetaMask 和 OKX Wallet,Wagmi 会列出所有可用的注入式钱包供用户选择。
与 TanStack Query 集成
Wagmi 的所有读取 Hooks 底层都是 TanStack Query,因此天然支持缓存、重试和后台刷新:
const { data, refetch } = useReadContract({
address: '0x...',
abi: erc20Abi,
functionName: 'balanceOf',
args: ['0x...'],
query: {
staleTime: 5_000, // 5 秒内不重新请求
refetchInterval: 10_000, // 每 10 秒自动刷新
retry: 3, // 失败重试 3 次
enabled: !!address, // 仅在有地址时查询
},
})
这意味着在同一页面中多个组件调用相同的 useReadContract(相同参数),实际只会发出一次 RPC 请求。
完整示例:Token Approve + Transfer 流程
以下是一个常见的 DApp 操作流程 —— 先授权(Approve),再转账(Transfer):
import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { erc20Abi, parseUnits } from 'viem'
const TOKEN = '0xTokenAddress...' as const
const SPENDER = '0xSpenderAddress...' as const
const AMOUNT = parseUnits('100', 18)
function ApproveAndTransfer({ to }: { to: `0x${string}` }) {
const { address } = useAccount()
// 1. 读取当前授权额度
const { data: allowance } = useReadContract({
address: TOKEN,
abi: erc20Abi,
functionName: 'allowance',
args: [address!, SPENDER],
query: { enabled: !!address },
})
// 2. 写合约 Hook(approve 和 transfer 共用)
const {
writeContract,
data: txHash,
isPending,
reset,
} = useWriteContract()
// 3. 等待交易确认
const { isLoading: isConfirming, isSuccess } =
useWaitForTransactionReceipt({ hash: txHash })
const needsApproval = allowance !== undefined && allowance < AMOUNT
function handleClick() {
if (needsApproval) {
// 授权额度不足,先 approve
writeContract({
address: TOKEN,
abi: erc20Abi,
functionName: 'approve',
args: [SPENDER, AMOUNT],
})
} else {
// 额度充足,直接 transfer
writeContract({
address: TOKEN,
abi: erc20Abi,
functionName: 'transfer',
args: [to, AMOUNT],
})
}
}
return (
<div>
<button onClick={handleClick} disabled={isPending || isConfirming}>
{isPending && '等待确认...'}
{isConfirming && '交易确认中...'}
{!isPending && !isConfirming && (needsApproval ? 'Approve' : 'Transfer')}
</button>
{isSuccess && <p>交易成功! 哈希:{txHash}</p>}
</div>
)
}
上面的示例为了简洁将 approve 和 transfer 放在一个 useWriteContract 中管理。在实际项目中,建议为每个交易步骤使用独立的 useWriteContract 实例,以便分别跟踪各步骤的状态。
常见模式
连接按钮组件
import { useAccount, useConnect, useDisconnect } from 'wagmi'
export function ConnectButton() {
const { isConnected, address } = useAccount()
const { connect, connectors } = useConnect()
const { disconnect } = useDisconnect()
if (isConnected) {
return (
<button onClick={() => disconnect()}>
{/* 截断显示地址 */}
{address?.slice(0, 6)}...{address?.slice(-4)}
</button>
)
}
return (
<div>
{connectors.map((c) => (
<button key={c.uid} onClick={() => connect({ connector: c })}>
{c.name}
</button>
))}
</div>
)
}
链切换
import { useSwitchChain } from 'wagmi'
function ChainSwitcher() {
const { chains, switchChain, isPending } = useSwitchChain()
return (
<div>
{chains.map((chain) => (
<button
key={chain.id}
onClick={() => switchChain({ chainId: chain.id })}
disabled={isPending}
>
{chain.name}
</button>
))}
</div>
)
}
乐观更新
利用 TanStack Query 的 onSuccess 回调,在交易发出后立即更新 UI,不等区块确认:
import { useQueryClient } from '@tanstack/react-query'
function useOptimisticTransfer() {
const queryClient = useQueryClient()
const { writeContract } = useWriteContract({
mutation: {
onSuccess() {
// 交易发出后立即使余额查询缓存失效,触发重新请求
queryClient.invalidateQueries({ queryKey: ['balance'] })
},
},
})
return writeContract
}
ConnectKit / RainbowKit / Web3Modal 对比
这些库都是基于 Wagmi 构建的钱包连接 UI 组件:
| 特性 | ConnectKit | RainbowKit | Web3Modal (Reown) |
|---|---|---|---|
| 维护方 | Family | Rainbow | WalletConnect |
| UI 定制化 | 高,多种主题 | 高,丰富的自定义 | 中等 |
| 钱包支持 | 依赖 Wagmi connectors | 内置 170+ 钱包 | WalletConnect 生态 |
| 包体积 | 较小 | 中等 | 中等 |
| ENS 头像 | 支持 | 支持 | 支持 |
| 链切换 UI | 内置 | 内置 | 内置 |
| 框架要求 | React | React | React / Vue / Vanilla |
| 底层 | Wagmi v2 | Wagmi v2 | Wagmi v2 / Ethers |
- 快速搭建 + 好看的 UI → RainbowKit
- 深度自定义 + 轻量 → ConnectKit
- 多框架支持 + WalletConnect 生态 → Web3Modal
- 完全控制 UI → 直接使用 Wagmi Hooks 自建
常见面试问题
Q1: Wagmi 是什么?为什么选择 Wagmi 而不是直接使用 Ethers.js/Viem?
答案:
Wagmi 是 React Hooks for Ethereum,底层基于 Viem 和 TanStack Query。相比直接使用 Ethers.js 或 Viem:
- 声明式:用 Hooks 描述"要什么数据",而不是命令式地"如何获取数据"
- 自动缓存:TanStack Query 提供请求去重、缓存、后台刷新
- 类型安全:从 ABI 自动推断参数和返回值类型
- 状态管理:自动处理
loading/error/success状态 - 钱包管理:统一的连接器抽象,一套代码支持多种钱包
Q2: Wagmi 的 Config 包含哪些核心配置?
答案:
createConfig 的三个核心配置:
- chains:应用支持的区块链列表(如 mainnet、polygon)
- transports:每条链的 RPC 传输方式(
http()、webSocket()) - connectors:钱包连接器(
injected、walletConnect、coinbaseWallet)
此外还支持 ssr(服务端渲染)、multiInjectedProviderDiscovery(自动发现注入式钱包)等可选配置。
Q3: useReadContract 和 useWriteContract 的区别是什么?
答案:
| 特性 | useReadContract | useWriteContract |
|---|---|---|
| 调用类型 | view / pure 函数 | 状态修改函数 |
| Gas 消耗 | 不消耗 | 消耗 Gas |
| 钱包签名 | 不需要 | 需要用户签名确认 |
| 返回值 | 合约函数的返回值 | 交易哈希 |
| 底层 | TanStack Query | Mutation |
| 缓存 | 自动缓存 | 无缓存(每次是新交易) |
Q4: 如何处理交易的完整生命周期?
答案:
一笔交易经历以下阶段:
- Idle → 初始状态
- Pending →
useWriteContract发起,等待用户在钱包中确认 - Submitted → 用户确认,获得交易哈希
- Confirming →
useWaitForTransactionReceipt等待区块确认 - Success / Error → 交易成功或失败
const { writeContract, data: hash, isPending, error } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })
Q5: Wagmi 是如何利用 TanStack Query 的?
答案:
Wagmi 的所有读取 Hooks(useReadContract、useBalance、useBlockNumber 等)底层都是 TanStack Query 的 useQuery。这带来了:
- 请求去重:同一页面多个组件读取相同数据,只发一次 RPC 请求
- 缓存:相同参数的查询自动复用缓存结果
- 后台刷新:窗口聚焦时自动刷新过期数据
- staleTime 控制:可自定义数据新鲜度时间
- 乐观更新:交易后可立即更新 UI 再等待确认
开发者可以通过 query 参数自定义所有 TanStack Query 行为。
Q6: 什么是连接器(Connector)?如何自定义连接器?
答案:
连接器是 Wagmi 对不同钱包接入方式的抽象。每个连接器实现统一的接口(connect、disconnect、getAccount、switchChain 等),让上层代码无需关心底层差异。
Wagmi 提供了 createConnector 函数用于创建自定义连接器,需要实现 setup、connect、disconnect、getAccounts、getChainId、getProvider 等方法。
Q7: 如何实现多步骤交易(如 Approve → Swap)?
答案:
核心思路是使用多个 useWriteContract 实例,按步骤串联:
- 先调用
useReadContract检查当前allowance - 如果 allowance 不足,调用第一个
useWriteContract执行approve - 用
useWaitForTransactionReceipt等待 approve 确认 - approve 成功后,调用第二个
useWriteContract执行swap - 再次等待交易确认
关键是通过 useEffect 监听上一步的 isSuccess 状态来触发下一步。
Q8: Wagmi v2 与 v1 的主要区别是什么?
答案:
- 底层从 Ethers.js 换为 Viem:更小的包体积、更好的类型安全
- TanStack Query v5:更好的 TypeScript 支持
- Hooks 重命名:如
useContractRead→useReadContract - 移除
prepare模式:不再需要usePrepareContractWrite - 多链支持增强:
useReadContract可以通过chainId参数指定查询哪条链 - 连接器 API 变化:v2 的连接器是函数调用(
injected()),v1 是new实例化
Q9: ConnectKit、RainbowKit、Web3Modal 应该如何选择?
答案:
三者都是基于 Wagmi 的钱包连接 UI 库,选择取决于需求:
- RainbowKit:最受欢迎,支持 170+ 钱包,UI 精美,适合需要快速集成的项目
- ConnectKit:Family 团队出品,体积更小,自定义程度高,适合注重性能的项目
- Web3Modal:WalletConnect 官方维护,支持 React / Vue / Vanilla JS,适合多框架项目
如果对 UI 有完全控制需求,建议直接使用 Wagmi 的 useConnect / useDisconnect 自建连接组件。
Q10: 如何处理用户切换链或切换账户的情况?
答案:
Wagmi 会自动监听钱包的 chainChanged 和 accountsChanged 事件,并更新所有相关 Hooks 的状态。开发者只需要:
- 通过
useAccount的chain和address响应式获取最新值 - 如果需要执行副作用,可以在
useEffect中监听这些值的变化 - 使用
useSwitchChain主动引导用户切换到正确的链
const { address, chain } = useAccount()
useEffect(() => {
if (chain && chain.id !== expectedChainId) {
// 提示用户切换链
}
}, [chain])
Q11: Wagmi 中如何实现 SSR(服务端渲染)?
答案:
Wagmi v2 原生支持 SSR,配置中设置 ssr: true 即可:
const config = createConfig({
ssr: true,
chains: [mainnet],
transports: { [mainnet.id]: http() },
})
SSR 模式下,Wagmi 会在服务端渲染时跳过钱包连接相关的逻辑(因为服务端没有 window.ethereum),水合后在客户端自动恢复连接状态。配合 TanStack Query 的 dehydrate / hydrate 可以实现数据预取。
Q12: 使用 Wagmi 时有哪些常见的性能优化手段?
答案:
- 合理设置
staleTime:避免频繁的 RPC 请求,如余额查询设 5-10 秒 - 条件查询:通过
enabled参数控制 Hook 是否执行查询 useReadContracts批量读取:一次 multicall 读取多个合约数据,减少 RPC 次数- 事件监听替代轮询:用
useWatchContractEvent替代refetchInterval - 按需导入:从
wagmi/connectors和wagmi/chains按需导入,减少包体积 queryClient.invalidateQueries:交易成功后精确失效相关缓存,而非全量刷新