用 wagmi v2 踩坑两天,我终于搞懂了多链钱包切换
摘要
跨链 DEX 项目里,用户切换以太坊主网到 BSC 后,RainbowKit 弹窗里的连接状态直接断了。我研究了 wagmi v2 的 useChain、useSwitchChain 和 createConfig 的配置细节,最终用 autoConnect + onStatus 回调实现无缝切换。这篇文章就是我当时解决问题的完整记录。
背景
今年初接了一个跨链聚合 DEX 的前端开发,需求是让用户可以用一个 MetaMask 钱包在 Ethereum、BSC、Polygon 三条链之间来回切换,每次换链后自动更新余额和交易对数据。我一开始想:RainbowKit 不是自带多链支持吗?直接照着文档配一下不就行了?结果一跑起来,用户从 Ethereum 切到 BSC,钱包弹窗里显示"Disconnected",再点连接又得重新授权,体验极其糟糕。我当时就踩了这个坑------RainbowKit 默认只管理钱包连接,但链切换后 wagmi 的 chainId 状态和钱包的 chainId 不一致,导致连接状态被重置。
问题分析
我最初的思路很简单:用 RainbowKit 的 chains 配置多链,然后在每个页面组件里调用 useAccount 获取连接状态,再用 useSwitchChain 来让用户手动切换。但测试时发现,用户切换到 BSC 后,useAccount 的 isConnected 变成了 false。我排查了半小时,打印了 useAccount 和 useNetwork 的所有字段,发现 chain 对象变了,但 connector 还在。这说明钱包本身没断开,但 wagmi 认为链不匹配,所以把连接状态清空了。
后来我翻 wagmi v2 的源码,看到 createConfig 里有个 multiInjectedProviderDiscovery 选项,默认是 true。这个选项会导致 wagmi 自动检测浏览器里所有的 injected provider(比如 MetaMask、Coinbase Wallet),然后在链切换时,它可能会把连接状态绑定到某个特定的 provider 实例上。如果用户切换链,provider 的 chainId 变了,wagmi 就认为连接失效。这就是问题的根源------wagmi v2 默认把链和连接强绑定,而 RainbowKit 没有自动处理这个绑定变化。
核心实现
1. 配置 wagmi 的 createConfig,避开自动检测的坑
第一步是修改 createConfig 的配置。这里有个关键点:multiInjectedProviderDiscovery 一定要设为 false,否则 wagmi 会反复创建多个 provider 实例,导致链切换时连接混乱。同时,connectors 里要显式指定 injected 作为默认连接器,而不是用 metaMask 这种特定钱包的连接器,因为 injected 会统一处理所有浏览器内置钱包。
typescript
// wagmi.config.ts
import { createConfig, http } from 'wagmi'
import { mainnet, bsc, polygon } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'
export const config = createConfig({
chains: [mainnet, bsc, polygon],
connectors: [
injected({
shimDisconnect: false, // 这个选项很重要,后面会讲
}),
],
transports: {
[mainnet.id]: http(),
[bsc.id]: http('https://bsc-dataseed.binance.org'),
[polygon.id]: http('https://polygon-rpc.com'),
},
multiInjectedProviderDiscovery: false, // 关闭自动发现
})
这里有个坑:shimDisconnect 默认是 true,它会在 localStorage 里存一个标志,用来模拟断开连接。但多链场景下,这个标志会在链切换时被误触发,导致连接状态丢失。我一开始没关这个,结果用户切链后 isConnected 总是 false,排查了半天才发现是它搞的鬼。
2. 用 onStatus 回调监听连接状态变化,实现自动重连
光配置好还不行,还得在应用层面监听链切换事件。wagmi v2 的 createConfig 返回一个 store 对象,它有一个 subscribe 方法,可以监听状态变化。我写了一个自定义 hook useAutoReconnect,在链切换时自动调用 reconnect 方法。
typescript
// hooks/useAutoReconnect.ts
import { useEffect } from 'react'
import { useConfig, useAccount } from 'wagmi'
export function useAutoReconnect() {
const config = useConfig()
const { chainId } = useAccount()
useEffect(() => {
// 监听 chainId 变化
const unsubscribe = config.subscribe(
(state) => state.chainId,
async (newChainId, prevChainId) => {
if (newChainId !== prevChainId && newChainId) {
console.log(`链切换: ${prevChainId} -> ${newChainId}`)
// 尝试重新连接,但不要弹窗
try {
await config.reconnect()
} catch (error) {
console.error('自动重连失败:', error)
}
}
}
)
return () => unsubscribe()
}, [config])
}
注意这个 config.subscribe 的用法:第一个参数是一个选择器函数,返回你要监听的状态片段;第二个参数是回调函数。这里我监听 state.chainId,一旦变化就执行重连。reconnect 方法会尝试用已有的连接器重新连接,不会弹出钱包授权窗口,用户体验很顺滑。
3. 在 RainbowKit 中集成自定义的链切换逻辑
RainbowKit 默认的链切换是通过 useSwitchChain 实现的,但它在多链场景下有个问题:它会让用户手动选择目标链,而不是自动跟随钱包的链切换。我需要让 RainbowKit 的链选择器与 wagmi 的链状态同步。
我重写了 RainbowKit 的 ChainSwitcher 组件,用 wagmi 的 useSwitchChain 替代默认实现,同时加上一个链切换确认弹窗,防止用户误操作。
typescript
// components/CustomChainSwitcher.tsx
import { useSwitchChain, useChainId } from 'wagmi'
import { mainnet, bsc, polygon } from 'wagmi/chains'
const chainMap = {
[mainnet.id]: { name: 'Ethereum', icon: '🟦' },
[bsc.id]: { name: 'BSC', icon: '🟨' },
[polygon.id]: { name: 'Polygon', icon: '🟪' },
}
export function CustomChainSwitcher() {
const currentChainId = useChainId()
const { switchChain } = useSwitchChain()
const handleSwitch = (chainId: number) => {
if (chainId === currentChainId) return
// 这里可以加一个确认弹窗,防止误切换
const confirm = window.confirm(`切换到 ${chainMap[chainId]?.name}?`)
if (!confirm) return
switchChain({ chainId })
}
return (
<div style={{ display: 'flex', gap: '8px', padding: '8px' }}>
{Object.entries(chainMap).map(([id, chain]) => (
<button
key={id}
onClick={() => handleSwitch(Number(id))}
style={{
padding: '8px 16px',
border: Number(id) === currentChainId ? '2px solid blue' : '1px solid gray',
borderRadius: '8px',
cursor: 'pointer',
}}
>
{chain.icon} {chain.name}
</button>
))}
</div>
)
}
这里有个细节:useSwitchChain 返回的 switchChain 函数需要传入 chainId,而且它会在底层调用钱包的 wallet_switchEthereumChain 方法。如果钱包不支持该链,它会自动尝试 wallet_addEthereumChain。但注意,有些钱包(比如 Trust Wallet)可能在切换 BSC 时失败,因为 BSC 的 chainId 不是标准的 EVM 链。我在测试时就遇到了这个情况,后面会讲怎么解决。
4. 处理钱包不支持目标链的 fallback
如果用户的钱包没有配置目标链,switchChain 会抛出一个错误。我需要捕获这个错误,并引导用户手动添加链。这里用了 useSwitchChain 的 onError 回调,或者直接在 switchChain 外面包一层 try-catch。
typescript
// hooks/useSafeSwitchChain.ts
import { useSwitchChain } from 'wagmi'
import { useCallback } from 'react'
export function useSafeSwitchChain() {
const { switchChain, isPending } = useSwitchChain({
mutation: {
onError: (error) => {
// 检查错误是否是因为链未添加
if (error.message?.includes('wallet_switchEthereumChain')) {
alert('请手动在钱包中添加该链,或切换到支持的链')
// 可以在这里打开一个帮助弹窗
} else {
console.error('链切换失败:', error)
}
},
},
})
const safeSwitch = useCallback((chainId: number) => {
try {
switchChain({ chainId })
} catch (error) {
// 这里捕获同步错误,但 switchChain 是异步的,所以 onError 更可靠
}
}, [switchChain])
return { safeSwitch, isPending }
}
注意:useSwitchChain 的 mutation 配置是在 wagmi v2 中新增的,它允许你传入 onSuccess、onError 等回调。这个比在外面用 try-catch 更优雅,因为 switchChain 本身是异步的,try-catch 只能捕获同步错误。
完整代码
下面是一个完整的可运行示例,包含所有关键部分:
typescript
// App.tsx
import { WagmiProvider, createConfig, http } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { RainbowKitProvider, darkTheme } from '@rainbow-me/rainbowkit'
import { mainnet, bsc, polygon } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'
import { CustomChainSwitcher } from './components/CustomChainSwitcher'
import { useAutoReconnect } from './hooks/useAutoReconnect'
import '@rainbow-me/rainbowkit/styles.css'
const queryClient = new QueryClient()
const config = createConfig({
chains: [mainnet, bsc, polygon],
connectors: [
injected({
shimDisconnect: false,
}),
],
transports: {
[mainnet.id]: http(),
[bsc.id]: http('https://bsc-dataseed.binance.org'),
[polygon.id]: http('https://polygon-rpc.com'),
},
multiInjectedProviderDiscovery: false,
})
function AppContent() {
useAutoReconnect() // 启用自动重连
return (
<div>
<CustomChainSwitcher />
{/* 其他组件 */}
</div>
)
}
export default function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider theme={darkTheme()}>
<AppContent />
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
)
}
typescript
// hooks/useAutoReconnect.ts
import { useEffect } from 'react'
import { useConfig, useAccount } from 'wagmi'
export function useAutoReconnect() {
const config = useConfig()
const { chainId } = useAccount()
useEffect(() => {
const unsubscribe = config.subscribe(
(state) => state.chainId,
async (newChainId, prevChainId) => {
if (newChainId !== prevChainId && newChainId) {
console.log(`链切换: ${prevChainId} -> ${newChainId}`)
try {
await config.reconnect()
} catch (error) {
console.error('自动重连失败:', error)
}
}
}
)
return () => unsubscribe()
}, [config])
}
踩坑记录
-
shimDisconnect导致连接状态丢失 :一开始没关这个选项,用户切链后isConnected总是false。排查了一上午,最后在 wagmi 的 GitHub issue 里看到有人说这个选项会在 localStorage 里存一个wagmi.disconnect标志,链切换时这个标志被误触发。关掉后就正常了。 -
BSC 链切换失败 :用 MetaMask 测试时,从 Ethereum 切到 BSC 没问题,但用 Trust Wallet 时,
switchChain直接抛错。原因是 Trust Wallet 默认没有添加 BSC 链,需要先调用wallet_addEthereumChain。我后来在onError回调里加了一个处理:如果错误是wallet_switchEthereumChain导致的,就弹窗提示用户手动添加链,或者自动调用wallet_addEthereumChain(但不同钱包的 RPC URL 可能不同,所以我选择了弹窗提示)。 -
config.subscribe在组件卸载后仍执行 :第一次写useAutoReconnect时,我没有在useEffect的清理函数里取消订阅,结果组件卸载后订阅还在运行,导致内存泄漏。后来加了return () => unsubscribe()才解决。这个坑在 wagmi 文档里有提到,但我一开始没仔细看。 -
RainbowKit 的
ChainSwitcher与自定义组件冲突 :我替换了 RainbowKit 默认的链选择器后,发现 RainbowKit 的弹窗里还有一个链选择器,两个同时存在,用户会迷惑。后来我在RainbowKitProvider里设置了showRecentTransactions={true}但隐藏了链选择器,只保留自定义的CustomChainSwitcher。具体做法是在RainbowKitProvider的chains配置里,把链列表设为只读。
小结
核心收获:wagmi v2 的多链切换本质是 chainId 和连接器实例的绑定问题,关闭 shimDisconnect 和 multiInjectedProviderDiscovery,再配合 config.subscribe 监听链变化自动重连,就能实现无缝切换。可以继续深挖的方向:用 viem 的 createPublicClient 替代 wagmi 的 http 传输层,实现更精细的链状态同步。