背景
上个月,我接手了一个多链DeFi聚合器前端项目的重构工作。这个项目需要支持 Ethereum、Arbitrum、Polygon 和 Base 四条链,用户可以在不同链之间无缝切换来查看和管理资产。之前的代码用的是 ethers.js + 自己封装的钱包连接按钮,维护起来特别头疼——每个新链上线都要手动加配置,钱包切换的逻辑散落在各个组件里,测试一次要连接断开钱包几十次。
团队决定用 RainbowKit 来统一钱包连接体验,毕竟它封装了连接按钮、网络切换模态框这些通用UI。我心想:“这还不简单?照着文档装个包,几行代码不就搞定了?” 结果,我低估了多链配置的复杂性,特别是当用户的钱包(比如 MetaMask)里预置了自定义网络时,问题就来了。
问题分析
我按照 RainbowKit 官方文档的“快速开始”,十分钟就搭出了一个漂亮的连接按钮。点击后能弹出钱包列表,连接 MetaMask 也很顺利。但当我尝试从 Ethereum 切换到 Arbitrum 时,奇怪的事情发生了:前端页面显示“已连接至 Arbitrum”,但 MetaMask 扩展却还停留在 Ethereum 主网,而且发交易时会失败。
我打开浏览器控制台,发现 wagmi 的 useAccount 钩子返回的 chainId 和我通过 window.ethereum.chainId 拿到的值不一致。前端状态是 Arbitrum (42161),但钱包实际还在 Ethereum (1)。我管这叫“幽灵网络”问题——前端以为自己在一个链上,但钱包却在另一个链上,用户操作必然失败。
最初的排查思路是:是不是 RainbowKit 的 chain 配置没传对?我反复检查了传给 getDefaultConfig 的链对象。后来发现,问题出在 wagmi 的配置模式和与钱包的同步机制上。RainbowKit 底层依赖 wagmi 进行状态管理,而 wagmi 默认的 config 如果不明确指定连接器(connector)的行为模式,它可能不会主动要求钱包切换网络。
核心实现
1. 正确的多链配置初始化
首先,我放弃了文档里那个最简单的 getDefaultConfig 调用。它虽然方便,但对多链场景的控制力不够。我决定手动构建 wagmi 的 config,并显式地配置连接器。
// src/config/wagmi.ts
import { http, createConfig } from 'wagmi';
import { mainnet, arbitrum, polygon, base } from 'wagmi/chains';
import { injected, walletConnect } from 'wagmi/connectors';
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
// 注意:这里不要用 getDefaultConfig,它隐藏了太多细节
// 我们手动创建 config 以便精细控制
export const config = createConfig({
chains: [mainnet, arbitrum, polygon, base], // 明确支持哪些链
transports: {
// 为每条链指定 RPC 端点
[mainnet.id]: http('https://eth.llamarpc.com'), // 建议用公共节点或自己的节点
[arbitrum.id]: http('https://arb1.arbitrum.io/rpc'),
[polygon.id]: http('https://polygon-rpc.com'),
[base.id]: http('https://mainnet.base.org'),
},
connectors: [
// 注入式连接器(如 MetaMask)
injected({
// 关键配置:让连接器去同步钱包的网络
target: 'metaMask',
}),
// 钱包连接连接器(WalletConnect)
walletConnect({
projectId: '你的 WalletConnect Cloud Project ID', // 必须去 walletconnect.com 申请
showQrModal: false, // RainbowKit 会自己处理二维码弹窗
}),
],
// 这个配置很重要,确保状态同步
ssr: false, // 我们做的是前端应用
});
这里有个坑:injected 连接器的 target 配置。如果不指定,某些钱包可能不会正确触发网络切换事件。我一开始漏了这行,导致 MetaMask 的网络变更事件没有被 wagmi 捕获。
2. 封装自定义的连接上下文组件
接下来,我创建了一个独立的 Provider 组件,用来包裹整个应用。这样可以把所有 Web3 相关的配置隔离在一个地方。
// src/providers/Web3Provider.tsx
import { ReactNode } from 'react';
import { WagmiProvider } from 'wagmi';
import { RainbowKitProvider, darkTheme } from '@rainbow-me/rainbowkit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from '@/config/wagmi';
// 创建 React Query 客户端,wagmi 用它来缓存数据
const queryClient = new QueryClient();
interface Web3ProviderProps {
children: ReactNode;
}
export function Web3Provider({ children }: Web3ProviderProps) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
theme={darkTheme({
accentColor: '#3B82F6', // 自定义主题色
borderRadius: 'medium',
})}
// 关键:设置初始链,避免未定义状态
initialChain={config.chains[0]}
// 这个模式决定了用户切换网络时的行为
modalSize="compact"
>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
注意 initialChain 这个配置。我一开始没设,结果应用刚加载时,useChainId() 返回 undefined,导致一些组件渲染报错。把它设为配置中的第一条链(这里是 Ethereum),确保了初始状态的稳定性。
3. 实现安全的链切换逻辑
在需要切换链的组件(比如一个网络选择下拉菜单)里,我不能再简单调用 switchChain 就完事了。必须处理用户拒绝切换、钱包不支持目标链等各种情况。
// src/components/NetworkSwitcher.tsx
import { useState } from 'react';
import { useSwitchChain, useAccount } from 'wagmi';
import { config } from '@/config/wagmi';
export function NetworkSwitcher() {
const { chainId } = useAccount();
const { chains, switchChain, isPending } = useSwitchChain();
const [error, setError] = useState<string | null>(null);
const handleSwitch = async (targetChainId: number) => {
setError(null); // 清空旧错误
try {
// 这里有个重要细节:switchChain 返回 Promise,必须 await
await switchChain({ chainId: targetChainId });
// 切换成功后,错误状态会被 wagmi 自动更新
} catch (err: any) {
// 错误处理是必须的!
console.error('切换链失败:', err);
// 用户拒绝了切换请求
if (err?.code === 4001) {
setError('用户拒绝了网络切换');
return;
}
// 钱包里没有添加这个网络
if (err?.code === 4902) {
// 这里可以触发添加网络的逻辑
setError('请先在钱包中添加该网络');
// 在实际项目中,这里可以调用 wallet_addEthereumChain RPC
return;
}
setError(`切换失败: ${err?.message || '未知错误'}`);
}
};
return (
<div>
<select
value={chainId || ''}
onChange={(e) => handleSwitch(Number(e.target.value))}
disabled={isPending}
>
<option value="" disabled>选择网络</option>
{chains.map((chain) => (
<option key={chain.id} value={chain.id}>
{chain.name} {isPending && chain.id === chainId ? '(切换中...)' : ''}
</option>
))}
</select>
{error && <div style={{ color: 'red' }}>{error}</div>}
</div>
);
}
最大的教训在这里:一定要处理 switchChain 的 Promise 拒绝。我一开始只用 switchChain({ chainId }) 而不 await,也没加 try-catch。结果用户拒绝切换时,前端状态已经更新了,但钱包没变,又回到了“幽灵网络”状态。
4. 关键:监听钱包网络变化并同步
为了解决“幽灵网络”问题,我添加了一个监听器组件,专门负责同步钱包和前端的网络状态。
// src/components/NetworkSync.tsx
import { useEffect } from 'react';
import { useAccount, useChainId } from 'wagmi';
// 这个组件不渲染任何UI,只负责副作用
export function NetworkSync() {
const { connector } = useAccount();
const chainId = useChainId();
useEffect(() => {
if (!connector) return;
// 监听钱包的网络变化事件
const handleChange = ({ chainId: newChainId }: { chainId?: number }) => {
if (newChainId && newChainId !== chainId) {
console.log(`钱包网络已切换至: ${newChainId}`);
// 这里不需要手动更新状态,wagmi 会处理
// 但可以在这里触发一些副作用,比如重新查询余额
}
};
// 注意:不同连接器的事件名可能不同
connector.on('change', handleChange);
return () => {
connector.off('change', handleChange);
};
}, [connector, chainId]);
return null; // 不渲染任何东西
}
这个组件放在 App 的根组件里。它确保当用户在 MetaMask 里手动切换网络时,前端状态能及时更新。我一开始以为 wagmi 会自动处理所有事件,后来发现某些边缘情况下(比如用户直接操作钱包扩展),事件传递会丢失。
5. 完整的应用集成
最后,我把所有部分组装起来:
// src/App.tsx
import { Web3Provider } from './providers/Web3Provider';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { NetworkSwitcher } from './components/NetworkSwitcher';
import { NetworkSync } from './components/NetworkSync';
import { useAccount } from 'wagmi';
function AppContent() {
const { isConnected } = useAccount();
return (
<div style={{ padding: '20px' }}>
<h1>多链 DeFi 聚合器</h1>
<div style={{ marginBottom: '20px' }}>
<ConnectButton />
</div>
<NetworkSync /> {/* 关键:同步网络状态 */}
{isConnected && (
<div style={{ marginTop: '20px' }}>
<h3>切换网络</h3>
<NetworkSwitcher />
</div>
)}
{/* 其他应用内容... */}
</div>
);
}
export default function App() {
return (
<Web3Provider>
<AppContent />
</Web3Provider>
);
}
完整代码
以下是完整的、可运行的示例,需要安装依赖:wagmi v2、@rainbow-me/rainbowkit、viem、@tanstack/react-query。
// 文件结构:
// src/
// ├── App.tsx
// ├── main.tsx (或 index.tsx)
// ├── providers/
// │ └── Web3Provider.tsx
// ├── config/
// │ └── wagmi.ts
// └── components/
// ├── NetworkSwitcher.tsx
// └── NetworkSync.tsx
// 1. 首先安装依赖:
// npm install wagmi viem @rainbow-me/rainbowkit @tanstack/react-query
// 2. src/config/wagmi.ts
import { http, createConfig } from 'wagmi';
import { mainnet, arbitrum, polygon, base } from 'wagmi/chains';
import { injected, walletConnect } from 'wagmi/connectors';
export const config = createConfig({
chains: [mainnet, arbitrum, polygon, base],
transports: {
[mainnet.id]: http(),
[arbitrum.id]: http(),
[polygon.id]: http(),
[base.id]: http(),
},
connectors: [
injected({ target: 'metaMask' }),
walletConnect({
projectId: 'YOUR_PROJECT_ID', // 替换为实际ID
showQrModal: false
}),
],
ssr: false,
});
// 3. src/providers/Web3Provider.tsx
import { ReactNode } from 'react';
import { WagmiProvider } from 'wagmi';
import { RainbowKitProvider, darkTheme } from '@rainbow-me/rainbowkit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from '@/config/wagmi';
const queryClient = new QueryClient();
interface Web3ProviderProps {
children: ReactNode;
}
export function Web3Provider({ children }: Web3ProviderProps) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
theme={darkTheme({ accentColor: '#3B82F6' })}
initialChain={config.chains[0]}
modalSize="compact"
>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
// 4. src/components/NetworkSwitcher.tsx
import { useState } from 'react';
import { useSwitchChain, useAccount } from 'wagmi';
export function NetworkSwitcher() {
const { chainId } = useAccount();
const { chains, switchChain, isPending } = useSwitchChain();
const [error, setError] = useState<string | null>(null);
const handleSwitch = async (targetChainId: number) => {
setError(null);
try {
await switchChain({ chainId: targetChainId });
} catch (err: any) {
console.error('切换链失败:', err);
if (err?.code === 4001) {
setError('用户拒绝了网络切换');
return;
}
if (err?.code === 4902) {
setError('请先在钱包中添加该网络');
return;
}
setError(`切换失败: ${err?.message || '未知错误'}`);
}
};
return (
<div>
<select
value={chainId || ''}
onChange={(e) => handleSwitch(Number(e.target.value))}
disabled={isPending}
>
<option value="" disabled>选择网络</option>
{chains.map((chain) => (
<option key={chain.id} value={chain.id}>
{chain.name} {isPending && chain.id === chainId ? '(切换中...)' : ''}
</option>
))}
</select>
{error && <div style={{ color: 'red' }}>{error}</div>}
</div>
);
}
// 5. src/components/NetworkSync.tsx
import { useEffect } from 'react';
import { useAccount, useChainId } from 'wagmi';
export function NetworkSync() {
const { connector } = useAccount();
const chainId = useChainId();
useEffect(() => {
if (!connector) return;
const handleChange = ({ chainId: newChainId }: { chainId?: number }) => {
if (newChainId && newChainId !== chainId) {
console.log(`钱包网络已切换至: ${newChainId}`);
// 可以在这里触发数据重新获取
}
};
connector.on('change', handleChange);
return () => {
connector.off('change', handleChange);
};
}, [connector, chainId]);
return null;
}
// 6. src/App.tsx
import { Web3Provider } from './providers/Web3Provider';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { NetworkSwitcher } from './components/NetworkSwitcher';
import { NetworkSync } from './components/NetworkSync';
import { useAccount } from 'wagmi';
function AppContent() {
const { isConnected } = useAccount();
return (
<div style={{ padding: '20px' }}>
<h1>多链 DeFi 聚合器</h1>
<div style={{ marginBottom: '20px' }}>
<ConnectButton />
</div>
<NetworkSync />
{isConnected && (
<div style={{ marginTop: '20px' }}>
<h3>切换网络</h3>
<NetworkSwitcher />
</div>
)}
</div>
);
}
export default function App() {
return (
<Web3Provider>
<AppContent />
</Web3Provider>
);
}
// 7. 入口文件 (如 src/main.tsx)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@rainbow-me/rainbowkit/styles.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
踩坑记录
-
“幽灵网络”问题:现象是前端显示一个链,钱包实际在另一个链。解决方法:添加
NetworkSync组件监听钱包事件,并在injected连接器中配置target: 'metaMask'确保事件正确传递。 -
WalletConnect 项目 ID 报错:控制台提示“Project ID required”。解决方法:必须去 walletconnect.com 注册并创建一个项目,获取真实的 Project ID,不能用示例中的占位符。
-
切换链时未处理用户拒绝:用户点击“拒绝”后,前端状态已更新但钱包未切换。解决方法:用
try-catch包裹switchChain调用,特别处理错误码4001(用户拒绝)。 -
初始加载时 chainId 为 undefined:应用刚加载时,
useChainId()返回undefined导致组件报错。解决方法:在RainbowKitProvider中设置initialChain={config.chains[0]}提供默认值。 -
TypeScript 类型错误:
connector.on('change', handler)提示类型不存在。解决方法:检查连接器类型,有些连接器的事件名可能是'chainChanged',需要查看具体连接器的文档或类型定义。
小结
这次集成让我明白,RainbowKit 虽然简化了UI,但多链状态同步的责任还在开发者肩上。核心收获是:必须显式处理网络切换的拒绝情况,并建立可靠的钱包事件监听机制。下一步可以继续优化用户体验,比如在钱包未添加网络时自动调用 wallet_addEthereumChain 来添加网络。