背景
上个月接了一个 NFT 交易市场的活,需求很明确:用户能连接钱包,在以太坊主网和 Polygon 之间切换,然后浏览和购买不同链上的 NFT。项目用的是 Next.js 14 + TypeScript,钱包接入我选了 wagmi v2,因为 v1 我用了大半年,觉得 API 挺顺手。
但没想到,v1 到 v2 的升级比我预想的要"伤筋动骨"得多。我拿着 v1 的思维去写 v2 的代码,结果就是各种类型报错、链切换后状态不同步、签名消息在 Polygon 上直接失败。那两天我基本就是"写代码 → 报错 → 查文档 → 改代码 → 新报错"的死循环。
这篇文章就是把我这两天的排查过程完整记录下来,特别是几个关键问题的解决思路和最终跑通的代码。如果你也在用 wagmi v2 做多链 DApp,希望我的踩坑记录能帮你省下至少一天时间。
问题分析
我最初的思路很简单:在 main.tsx 里配置好 http() 的 transport,然后每个页面组件里直接用 useAccount、useSwitchChain 这些 hooks。v1 里就是这么干的,所以我以为 v2 也差不多。
结果第一个报错就给我来了个下马威——TypeError: Cannot read properties of undefined (reading 'chains')。我一看,这报错出现在 createConfig 里。查了文档才发现,v2 的 createConfig 从 viem 包移到了 @wagmi/core,而且 chains 数组的传入方式变了,必须用 viem/chains 里导出的链对象,不能自己随便定义。
我一开始是这么写的:
// ❌ 错误写法
import { createConfig } from 'wagmi'
import { mainnet, polygon } from 'wagmi/chains'
但 v2 里 wagmi/chains 已经不存在了,需要从 viem/chains 导入。而且 createConfig 的 multiInjectedProviderDiscovery 选项在 v2 里默认就是 true,不需要显式声明。
第二个让我头疼的问题是链切换。我在组件里用 useSwitchChain 切换网络后,useAccount 返回的 chainId 确实变了,但页面上显示的 NFT 列表还是旧链的数据。这个问题的根源在于我用了 useEffect 去监听 chainId 变化再重新拉数据,但 useEffect 的依赖数组写错了,导致它根本没重新执行。
核心实现
第一步:正确的多链配置
先说结论,v2 的 createConfig 必须这样写:
// config/wagmi.ts
import { http, createConfig } from '@wagmi/core'
import { mainnet, polygon } from 'viem/chains'
import { metaMask } from 'wagmi/connectors'
export const wagmiConfig = createConfig({
chains: [mainnet, polygon],
connectors: [
metaMask(),
// 如果你还需要 walletConnect,可以加,但注意需要 projectId
// walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! })
],
transports: {
[mainnet.id]: http(),
[polygon.id]: http(),
},
})
这里有个坑:transports 的 key 必须是链的 id 数字,不能是链名。我第一次写成了 'mainnet': http(),结果 wagmi 内部完全找不到对应的 transport,导致连接钱包后所有 RPC 请求都超时。
然后在项目的根组件里:
// app/providers.tsx
'use client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { wagmiConfig } from '@/config/wagmi'
const queryClient = new QueryClient()
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
)
}
注意 v2 的 WagmiProvider 必须搭配 @tanstack/react-query 的 QueryClientProvider,否则 useReadContract 这类 hooks 会直接报错。这是我踩的第二个坑,后面细说。
第二步:链切换和状态同步
链切换的核心逻辑在 useSwitchChain 这个 hook 上。但我在实际项目中发现,切换链之后,页面上其他组件(比如 NFT 列表、价格显示)不会自动更新,因为 chainId 变化并没有触发所有相关组件的重新渲染。
我的解决方案是:在顶层组件里监听链变化,然后通过 Context 把当前链信息传下去。
// hooks/useActiveChain.ts
'use client'
import { useAccount, useSwitchChain } from 'wagmi'
import { createContext, useContext, useEffect, useState } from 'react'
interface ChainContextValue {
chainId: number | undefined
chainName: string
switchChain: (id: number) => Promise<void>
isSwitching: boolean
}
const ChainContext = createContext<ChainContextValue>({
chainId: undefined,
chainName: '',
switchChain: async () => {},
isSwitching: false,
})
export function ChainProvider({ children }: { children: React.ReactNode }) {
const { chainId, isConnected } = useAccount()
const { switchChainAsync, isPending } = useSwitchChain()
const [activeChainId, setActiveChainId] = useState<number | undefined>(chainId)
// 关键:同步 chainId 到本地 state
useEffect(() => {
if (chainId) {
setActiveChainId(chainId)
// 同时把 chainId 存到 localStorage,刷新页面后还能保持
localStorage.setItem('activeChainId', String(chainId))
}
}, [chainId])
const switchChain = async (id: number) => {
try {
await switchChainAsync({ chainId: id })
// switchChainAsync 成功后,chainId 会自动更新,useEffect 会同步
} catch (error) {
console.error('切换链失败:', error)
}
}
const chainName = activeChainId === 1 ? 'Ethereum' : activeChainId === 137 ? 'Polygon' : 'Unknown'
return (
<ChainContext.Provider value={{
chainId: activeChainId,
chainName,
switchChain,
isSwitching: isPending,
}}>
{children}
</ChainContext.Provider>
)
}
export const useActiveChain = () => useContext(ChainContext)
这里有个坑:useSwitchChain 返回的是 switchChainAsync 而不是 v1 里的 switchChain。v1 的 switchChain 是同步的,v2 的 switchChainAsync 是异步的,必须 await。我当时没注意,直接调 switchChain({ chainId: 137 }),结果链根本没切换成功,页面也没反应。
第三步:读取 NFT 数据时的多链处理
链切换搞定了,接下来是 NFT 列表的读取。我用的合约是 ERC-721,需要根据当前链的地址去调合约。
// components/NFTList.tsx
'use client'
import { useReadContract } from 'wagmi'
import { useActiveChain } from '@/hooks/useActiveChain'
import { nftABI } from '@/contracts/nftABI'
// 不同链上的 NFT 合约地址
const CONTRACT_ADDRESSES: Record<number, `0x${string}`> = {
1: '0xYourMainnetContract', // 以太坊主网合约
137: '0xYourPolygonContract', // Polygon 合约
}
export function NFTList() {
const { chainId } = useActiveChain()
const contractAddress = chainId ? CONTRACT_ADDRESSES[chainId] : undefined
const { data: totalSupply, refetch } = useReadContract({
address: contractAddress,
abi: nftABI,
functionName: 'totalSupply',
})
// 链切换后,需要手动触发 refetch
useEffect(() => {
refetch()
}, [chainId, refetch])
if (!contractAddress) {
return <div>请先连接钱包</div>
}
return (
<div>
<p>当前链上有 {totalSupply?.toString() ?? '...'} 个 NFT</p>
{/* 这里渲染 NFT 列表 */}
</div>
)
}
注意这个细节:useReadContract 的 address 参数如果是 undefined,wagmi 会直接跳过执行,不会报错。但如果你把 address 写死成某个链的地址,然后在另一条链上调用,合约调用会直接失败,因为合约不存在。我一开始就犯了这个问题,在 Polygon 上调用主网合约地址,useReadContract 返回的 error 是 ContractFunctionExecutionError,排查了半天。
第四步:签名消息的链上校验
交易市场还有一个核心功能:用户对 NFT 出价时,需要签名一条消息。但问题是,用户在 Polygon 上签名的消息,和以太坊主网上签名的消息,格式应该不一样。我当时没注意,直接把主网的签名逻辑搬到了 Polygon 上,结果用户签名后,后端验签一直失败。
后来我查了 EIP-712 的规范,发现 domain 里的 chainId 必须和当前链一致。我的代码里写死了 chainId: 1,所以在 Polygon 上签名,验签肯定过不了。
// utils/signBid.ts
import { useSignTypedData } from 'wagmi'
import { useActiveChain } from '@/hooks/useActiveChain'
export function useSignBid() {
const { chainId } = useActiveChain()
const { signTypedDataAsync } = useSignTypedData()
const signBid = async (nftId: number, price: string, nonce: number) => {
// 注意:domain 里的 chainId 必须动态获取
const domain = {
name: 'NFTMarketplace',
version: '1',
chainId: chainId, // 这里不能写死
verifyingContract: '0xYourMarketplaceContract',
}
const types = {
Bid: [
{ name: 'nftId', type: 'uint256' },
{ name: 'price', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
],
}
const message = {
nftId,
price,
nonce,
}
const signature = await signTypedDataAsync({
domain,
types,
primaryType: 'Bid',
message,
})
return signature
}
return { signBid }
}
这个坑让我浪费了整整一个下午。如果你也在做需要签名验证的功能,一定记得把 chainId 动态传入,不要写死。
完整代码
这里是一个最小可运行的完整示例,包含多链配置、链切换、NFT 读取三个功能:
// app/page.tsx
'use client'
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { useActiveChain, ChainProvider } from '@/hooks/useActiveChain'
import { NFTList } from '@/components/NFTList'
function WalletConnector() {
const { address, isConnected } = useAccount()
const { connect, connectors } = useConnect()
const { disconnect } = useDisconnect()
const { chainId, chainName, switchChain, isSwitching } = useActiveChain()
if (!isConnected) {
return (
<div>
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => connect({ connector })}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
连接 {connector.name}
</button>
))}
</div>
)
}
return (
<div>
<p>地址: {address}</p>
<p>当前链: {chainName} (ID: {chainId})</p>
<button
onClick={() => switchChain(1)}
disabled={isSwitching}
className="mr-2 px-4 py-2 bg-green-500 text-white rounded"
>
切换到以太坊
</button>
<button
onClick={() => switchChain(137)}
disabled={isSwitching}
className="mr-2 px-4 py-2 bg-purple-500 text-white rounded"
>
切换到 Polygon
</button>
<button onClick={() => disconnect()} className="px-4 py-2 bg-red-500 text-white rounded">
断开连接
</button>
<NFTList />
</div>
)
}
export default function Home() {
return (
<ChainProvider>
<WalletConnector />
</ChainProvider>
)
}
记得在 app/layout.tsx 里引入 Providers:
// app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
踩坑记录
1. Cannot read properties of undefined (reading 'chains')
原因:createConfig 从 wagmi 导入,但 v2 里应该从 @wagmi/core 导入。而且 wagmi/chains 在 v2 中不存在,要从 viem/chains 导入链对象。
2. 切换链后页面数据不更新
原因:useEffect 的依赖数组没写对。我一开始写的是 useEffect(() => { refetch() }, []),空依赖数组导致只在组件挂载时执行一次。改成 [chainId, refetch] 后正常。
3. 在 Polygon 上签名,后端验签失败
原因:EIP-712 的 domain.chainId 写死成了 1。必须动态获取当前链的 ID。这个错误不会在前端报错,但后端验签一定会失败,排查起来很隐蔽。
4. useReadContract 在未连接钱包时返回 undefined,页面报错
原因:我没判断 address 是否为 undefined,直接渲染 totalSupply.toString()。v2 的 useReadContract 在 address 为 undefined 时会跳过执行,但返回的 data 也是 undefined,需要加空值判断。
小结
wagmi v2 相比 v1 最大的变化就是 config 的创建方式和链对象的导入来源,以及所有 hooks 都变成了异步。多链切换的核心就是动态管理 chainId,并且所有依赖链的合约调用、签名操作都要基于当前链 ID 来执行。想继续深入的话,可以研究一下 wagmi v2 的 useAction 和 useMutation 在交易场景中的应用,对交易状态管理会很有帮助。