背景
上个月,我接手了一个"Uniswap 精简版"项目------一个支持 Ethereum、Polygon、Arbitrum 三条链的 DEX 前端。项目用 wagmi v2 + RainbowKit 做钱包连接,React + Vite 开发。需求听起来很简单:用户连接钱包后,能选择任意一条链进行交易,并且钱包会自动切换到对应链。
我当时想,wagmi 不是有 useSwitchChain 和 useAccount 吗?直接调用就完事了。结果呢?我花了整整三天,经历了无数个"为什么钱包没反应"、"为什么链没切换但页面状态变了"的抓狂时刻。这篇文章,就是把我踩过的坑和最终的解决方案完整记录下来。
问题分析
一开始,我的思路很直接:用 useAccount 获取当前链 ID,用 useSwitchChain 切换链。代码大概长这样:
typescript
// 我最初的错误写法
const { chain } = useAccount();
const { switchChain } = useSwitchChain();
const handleChainChange = (targetChainId: number) => {
if (chain?.id !== targetChainId) {
switchChain({ chainId: targetChainId });
}
};
看起来没问题对吧?但实际运行时,问题来了:
问题 1: 在 MetaMask 上切换链后,useAccount 返回的 chain 更新了,但 UI 上的交易对信息没有更新。我明明用了 useEffect 监听 chain 变化,但页面就是不刷新。
问题 2: 切换到一条不支持的链(比如用户自己添加了 BSC)时,useSwitchChain 会报错,但错误信息非常不友好,而且 chain 状态会被污染。
问题 3: 最诡异的是------当用户手动在钱包里切换链,而不是通过我写的按钮切换时,useSwitchChain 根本不会触发,但 useAccount 的 chain 变了。这就导致我的代码里有两套"当前链":一套来自按钮操作,一套来自钱包事件,它们经常不同步。
排查了两天,我翻遍了 wagmi 的文档和 GitHub Issues,终于发现了关键点:wagmi v2 中 useAccount 的 chain 是只读的,它只反映钱包当前连接的链,不会触发 React 组件的重新渲染 (至少在特定场景下)。而 useSwitchChain 返回的 isSuccess 状态才是可靠的切换完成标志。
核心实现
1. 重新理解 wagmi v2 的状态管理
我做的第一件事,是抛弃了"用 useAccount 驱动 UI"的思维。wagmi v2 推荐的做法是:用 useChainId 获取当前链 ID,用 useSwitchChain 处理切换,用 useEffect 监听切换完成事件。
这里有个坑:useChainId 返回的是 wagmi 配置中的当前链 ID,而不是钱包实际连接的链 ID。如果用户手动在钱包里切换,useChainId 不会自动更新!所以,我最终决定自己维护一个"同步的链状态"。
我创建了一个自定义 hook useSyncedChain:
typescript
// hooks/useSyncedChain.ts
import { useChainId, useSwitchChain, useAccount, usePublicClient } from 'wagmi';
import { useEffect, useState, useCallback } from 'react';
export function useSyncedChain() {
// 从 wagmi 获取基础状态
const configChainId = useChainId(); // wagmi 配置中的链 ID
const { chain: accountChain, isConnected } = useAccount(); // 钱包实际连接的链
const { switchChain, isPending, error } = useSwitchChain();
const publicClient = usePublicClient(); // 用来做链验证
// 我们自己的"权威"链 ID
const [activeChainId, setActiveChainId] = useState<number>(configChainId);
// 核心逻辑:同步钱包状态和配置状态
useEffect(() => {
if (!isConnected || !accountChain) {
// 未连接时,使用配置默认链
setActiveChainId(configChainId);
return;
}
// 如果钱包连接的链和配置链不同,说明用户手动切换了
if (accountChain.id !== configChainId) {
// 这里有个坑:不要直接 setActiveChainId,因为配置链可能不支持
// 应该检查 accountChain 是否在我们支持的链列表中
const supportedChains = [1, 137, 42161]; // Ethereum, Polygon, Arbitrum
if (supportedChains.includes(accountChain.id)) {
setActiveChainId(accountChain.id);
} else {
// 不支持的话,尝试切回默认链
switchChain({ chainId: configChainId });
}
} else {
setActiveChainId(configChainId);
}
}, [configChainId, accountChain, isConnected, switchChain]);
// 封装的切换函数
const switchToChain = useCallback(async (targetChainId: number) => {
try {
await switchChain({ chainId: targetChainId });
// switchChain 成功后,wagmi 会自动更新 configChainId
// 但为了保险,我们手动更新
setActiveChainId(targetChainId);
} catch (err) {
console.error('切换链失败:', err);
throw err;
}
}, [switchChain]);
return {
activeChainId,
switchToChain,
isSwitching: isPending,
error,
};
}
这个 hook 的核心思路是:不要信任任何一个单一来源,而是用钱包状态、配置状态、用户操作事件三者做交叉验证。
2. 处理链切换后的数据刷新
链切换后,我们需要重新获取交易对数据、用户余额等。一开始我用 useEffect 监听 activeChainId,但发现会触发两次:一次是状态更新,一次是钱包实际切换完成。
后来我用了 wagmi 的 useWatchChainId 来做精细控制:
typescript
// hooks/useChainDataRefresh.ts
import { useEffect, useRef } from 'react';
import { useChainId } from 'wagmi';
export function useChainDataRefresh(callback: (chainId: number) => void) {
const chainId = useChainId();
const prevChainIdRef = useRef(chainId);
useEffect(() => {
// 只在链真正变化时触发,避免初始化时的重复调用
if (prevChainIdRef.current !== chainId) {
console.log(`链已切换: ${prevChainIdRef.current} -> ${chainId}`);
callback(chainId);
prevChainIdRef.current = chainId;
}
}, [chainId, callback]);
}
然后在组件中使用:
typescript
// 在 Swap 组件中
const { activeChainId, switchToChain, isSwitching } = useSyncedChain();
const { data: pairData, refetch: refetchPair } = useQuery({
queryKey: ['pair', activeChainId, tokenA, tokenB],
queryFn: () => fetchPairData(activeChainId, tokenA, tokenB),
enabled: !!activeChainId && !!tokenA && !!tokenB,
});
useChainDataRefresh((newChainId) => {
// 链切换后,重新获取数据
refetchPair();
// 同时重置用户输入状态
setTokenA('');
setTokenB('');
});
3. 处理钱包手动切换和 UI 同步
最头疼的是用户手动在 MetaMask 里切换链。wagmi v2 的 useAccount 会更新,但 useChainId 不会。我之前的 useSyncedChain hook 已经通过 accountChain 处理了这种情况,但还有一个细节:切换完成后,需要等待钱包确认,期间 UI 应该显示加载状态。
我添加了一个"切换中"的状态管理:
typescript
// 在 useSyncedChain 中增加 pendingChainId
const [pendingChainId, setPendingChainId] = useState<number | null>(null);
const switchToChain = useCallback(async (targetChainId: number) => {
setPendingChainId(targetChainId);
try {
await switchChain({ chainId: targetChainId });
setPendingChainId(null);
setActiveChainId(targetChainId);
} catch (err) {
setPendingChainId(null);
throw err;
}
}, [switchChain]);
// 在 UI 中显示加载
const isLoading = isSwitching || pendingChainId !== null;
4. 最终的多链切换组件
把所有逻辑整合到一个组件中:
typescript
// components/ChainSwitcher.tsx
import { useSyncedChain } from '../hooks/useSyncedChain';
import { useChainDataRefresh } from '../hooks/useChainDataRefresh';
import { useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
const SUPPORTED_CHAINS = [
{ id: 1, name: 'Ethereum', nativeCurrency: 'ETH' },
{ id: 137, name: 'Polygon', nativeCurrency: 'MATIC' },
{ id: 42161, name: 'Arbitrum', nativeCurrency: 'ETH' },
];
export function ChainSwitcher() {
const { activeChainId, switchToChain, isSwitching, error } = useSyncedChain();
// 链切换后刷新数据
useChainDataRefresh((chainId) => {
console.log('链已切换,刷新数据');
// 这里可以触发其他数据获取
});
const handleChainClick = async (chainId: number) => {
if (chainId === activeChainId) return;
try {
await switchToChain(chainId);
// 切换成功后,UI 会自动更新,因为 activeChainId 变了
} catch (err) {
// 显示错误 toast
alert(`切换失败: ${(err as Error).message}`);
}
};
return (
<div>
<h2>选择链</h2>
{SUPPORTED_CHAINS.map((chain) => (
<button
key={chain.id}
onClick={() => handleChainClick(chain.id)}
disabled={isSwitching}
style={{
fontWeight: chain.id === activeChainId ? 'bold' : 'normal',
opacity: isSwitching ? 0.5 : 1,
}}
>
{chain.name} ({chain.nativeCurrency})
{isSwitching && ' 切换中...'}
</button>
))}
{error && <p style={{ color: 'red' }}>错误: {error.message}</p>}
</div>
);
}
完整代码
我把所有代码整合到一个可运行的示例中。假设你使用 Vite + React + TypeScript,安装依赖:
bash
npm install wagmi viem @tanstack/react-query react
typescript
// main.tsx - 入口文件
import { WagmiProvider, createConfig, http } from 'wagmi';
import { mainnet, polygon, arbitrum } from 'wagmi/chains';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit';
import { ChainSwitcher } from './components/ChainSwitcher';
const config = createConfig({
chains: [mainnet, polygon, arbitrum],
transports: {
[mainnet.id]: http(),
[polygon.id]: http(),
[arbitrum.id]: http(),
},
});
const queryClient = new QueryClient();
function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
<ChainSwitcher />
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
export default App;
typescript
// hooks/useSyncedChain.ts - 上面已给出完整代码
// hooks/useChainDataRefresh.ts - 上面已给出完整代码
// components/ChainSwitcher.tsx - 上面已给出完整代码
踩坑记录
坑 1:useAccount 的 chain 在切换后不会立即更新 现象:调用 switchChain 后,useAccount 返回的 chain 还是旧的,导致 UI 显示错误。解决:用 useChainId 配合 useEffect 监听,而不是依赖 useAccount 的 chain。
坑 2:useSwitchChain 的 isSuccess 有时为 false 现象:钱包已经切换成功,但 isSuccess 一直是 false。原因:wagmi v2 中 isSuccess 只在第一次成功时为 true,后续切换不会重置。解决:用 error 和 isPending 做判断,或者自己维护状态。
坑 3:在非浏览器环境(如测试时)调用 switchChain 会报错 现象:在 Node.js 或 React Native 中,window.ethereum 不存在,导致切换失败。解决:用 try-catch 包裹,并在错误时回退到配置默认链。
坑 4:链切换后,之前订阅的事件没有清理 现象:切换到 Polygon 后,Ethereum 上的事件监听还在运行,导致内存泄漏。解决:在 useEffect 中返回清理函数,或者用 wagmi 的 watchContractEvent 自动管理。
小结
多链切换的核心不是调用 switchChain,而是同步钱包状态、配置状态和用户操作状态 。wagmi v2 提供了基础工具,但需要自己组合成可靠的解决方案。如果你也遇到类似问题,可以试试我写的 useSyncedChain hook,或者深入看看 wagmi 的源码------里面有很多有趣的细节。
接下来,你可以探索如何用 wagmi 的 watchChainId 做更精细的控制,或者结合 viem 的 publicClient 做链验证。