跳到主要内容

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(调起钱包)、useDisconnectuseSwitchChain(切链)。
  • 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,基于 ViemTanStack Query 构建,提供了类型安全、可组合的 Hooks,让 React 应用能够优雅地完成钱包连接、余额查询、合约交互等 Web3 操作。

核心价值

Wagmi 将底层的 JSON-RPC 调用和钱包交互抽象为声明式的 React Hooks,同时利用 TanStack Query 的缓存和状态管理能力,让 DApp 开发体验接近传统 Web 应用。

架构设计

Wagmi 采用 Config → Provider → Hooks 三层架构:

层级职责核心 API
配置层定义链、传输、连接器createConfig
Provider 层注入配置和查询客户端WagmiProvider + QueryClientProvider
Hooks 层声明式读写区块链useAccountuseReadContract

配置

createConfig

createConfig 是 Wagmi 的入口,定义了应用支持的链、传输方式和钱包连接器:

config.ts
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 配置

在应用根组件中包裹 WagmiProviderQueryClientProvider

app.tsx
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>
)
}
必须同时提供两个 Provider

WagmiProvider 依赖 QueryClientProvider,缺少任何一个都会导致 Hooks 报错。TanStack Query 提供了缓存、重试、后台刷新等能力。

核心 Hooks

useAccount — 连接状态

获取当前连接的账户信息:

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 — 余额查询

查询 ETH 和 ERC-20 余额
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:

读取 ERC-20 合约
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:

ERC-20 Approve
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 — 监听合约事件

监听 Transfer 事件
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.ethereumMetaMask、OKX Wallet 等浏览器扩展
walletConnect()WalletConnect v2 协议移动端钱包扫码连接
coinbaseWallet()Coinbase Wallet SDKCoinbase 用户
safe()Gnosis Safe多签钱包
metaMask()MetaMask SDKMetaMask 专用增强功能
自动检测

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):

ApproveAndTransfer.tsx
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 实例,以便分别跟踪各步骤的状态。

常见模式

连接按钮组件

ConnectButton.tsx
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 组件:

特性ConnectKitRainbowKitWeb3Modal (Reown)
维护方FamilyRainbowWalletConnect
UI 定制化高,多种主题高,丰富的自定义中等
钱包支持依赖 Wagmi connectors内置 170+ 钱包WalletConnect 生态
包体积较小中等中等
ENS 头像支持支持支持
链切换 UI内置内置内置
框架要求ReactReactReact / Vue / Vanilla
底层Wagmi v2Wagmi v2Wagmi 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 的三个核心配置:

  1. chains:应用支持的区块链列表(如 mainnet、polygon)
  2. transports:每条链的 RPC 传输方式(http()webSocket()
  3. connectors:钱包连接器(injectedwalletConnectcoinbaseWallet

此外还支持 ssr(服务端渲染)、multiInjectedProviderDiscovery(自动发现注入式钱包)等可选配置。

Q3: useReadContractuseWriteContract 的区别是什么?

答案

特性useReadContractuseWriteContract
调用类型view / pure 函数状态修改函数
Gas 消耗不消耗消耗 Gas
钱包签名不需要需要用户签名确认
返回值合约函数的返回值交易哈希
底层TanStack QueryMutation
缓存自动缓存无缓存(每次是新交易)

Q4: 如何处理交易的完整生命周期?

答案

一笔交易经历以下阶段:

  1. Idle → 初始状态
  2. PendinguseWriteContract 发起,等待用户在钱包中确认
  3. Submitted → 用户确认,获得交易哈希
  4. ConfirminguseWaitForTransactionReceipt 等待区块确认
  5. Success / Error → 交易成功或失败
const { writeContract, data: hash, isPending, error } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })

Q5: Wagmi 是如何利用 TanStack Query 的?

答案

Wagmi 的所有读取 Hooks(useReadContractuseBalanceuseBlockNumber 等)底层都是 TanStack Query 的 useQuery。这带来了:

  • 请求去重:同一页面多个组件读取相同数据,只发一次 RPC 请求
  • 缓存:相同参数的查询自动复用缓存结果
  • 后台刷新:窗口聚焦时自动刷新过期数据
  • staleTime 控制:可自定义数据新鲜度时间
  • 乐观更新:交易后可立即更新 UI 再等待确认

开发者可以通过 query 参数自定义所有 TanStack Query 行为。

Q6: 什么是连接器(Connector)?如何自定义连接器?

答案

连接器是 Wagmi 对不同钱包接入方式的抽象。每个连接器实现统一的接口(connectdisconnectgetAccountswitchChain 等),让上层代码无需关心底层差异。

Wagmi 提供了 createConnector 函数用于创建自定义连接器,需要实现 setupconnectdisconnectgetAccountsgetChainIdgetProvider 等方法。

Q7: 如何实现多步骤交易(如 Approve → Swap)?

答案

核心思路是使用多个 useWriteContract 实例,按步骤串联:

  1. 先调用 useReadContract 检查当前 allowance
  2. 如果 allowance 不足,调用第一个 useWriteContract 执行 approve
  3. useWaitForTransactionReceipt 等待 approve 确认
  4. approve 成功后,调用第二个 useWriteContract 执行 swap
  5. 再次等待交易确认

关键是通过 useEffect 监听上一步的 isSuccess 状态来触发下一步。

Q8: Wagmi v2 与 v1 的主要区别是什么?

答案

  • 底层从 Ethers.js 换为 Viem:更小的包体积、更好的类型安全
  • TanStack Query v5:更好的 TypeScript 支持
  • Hooks 重命名:如 useContractReaduseReadContract
  • 移除 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 会自动监听钱包的 chainChangedaccountsChanged 事件,并更新所有相关 Hooks 的状态。开发者只需要:

  • 通过 useAccountchainaddress 响应式获取最新值
  • 如果需要执行副作用,可以在 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 时有哪些常见的性能优化手段?

答案

  1. 合理设置 staleTime:避免频繁的 RPC 请求,如余额查询设 5-10 秒
  2. 条件查询:通过 enabled 参数控制 Hook 是否执行查询
  3. useReadContracts 批量读取:一次 multicall 读取多个合约数据,减少 RPC 次数
  4. 事件监听替代轮询:用 useWatchContractEvent 替代 refetchInterval
  5. 按需导入:从 wagmi/connectorswagmi/chains 按需导入,减少包体积
  6. queryClient.invalidateQueries:交易成功后精确失效相关缓存,而非全量刷新

相关链接