把已有的React应用迁到Avalanche上,并不需要重写UI或放弃现有的状态管理方案。Avalanche C链完全兼容以太坊虚拟机,所以Ethers.js、Web3Modal这些工具库可以直接复用,只需要把网络节点从以太坊切换到Avalanche,并用AVAX支付Gas。迁移的核心工作集中在三处:替换Provider配置、部署并导出Solidity合约ABI、在组件中封装合约读写逻辑。下面从网络接入开始,逐步完成一个可运行的前端。

一、切换Avalanche网络:从RPC到链ID
Avalanche的C链完全兼容以太坊虚拟机,主网链ID为43114,Fuji测试网链ID为43113。无论你之前用的是MetaMask还是其他EVM钱包,只要在React前端中写入正确的链ID和RPC地址,用户就能一键切换到Avalanche网络。与以太坊不同,Avalanche上的Gas费以AVAX结算,因此迁移前最好先通过官方水龙头领取一些Fuji测试币。
依赖方面,建议保持现有React工程结构不变,只增加两个包:ethers用于合约交互和Provider封装,web3modal负责钱包连接弹窗。安装命令如下。
npm install ethers web3modal
接着在项目的配置文件中定义一个网络参数对象。这个对象会传给浏览器的wallet_addEthereumChain方法,让钱包自动识别Avalanche网络,避免用户手动输入RPC。
const avalancheFuji = {
chainId: '0xA869',
chainName: 'Avalanche Fuji Testnet',
rpcUrls: ['https://api.avax-test.network/ext/bc/C/rpc'],
nativeCurrency: { name: 'AVAX', symbol: 'AVAX', decimals: 18 },
blockExplorerUrls: ['https://testnet.snowtrace.io']
};
if (window.ethereum) {
try {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [avalancheFuji]
});
} catch (error) {
console.error('网络切换失败', error);
}
}
这段代码可以在用户点击连接钱包后调用。如果用户已经添加过Fuji网络,钱包会直接返回已存在的网络;如果没添加,就会弹出确认窗口。只有网络切换成功后,后续的合约读取和写入才能作用在Avalanche链上。
二、部署Solidity合约并导出ABI
为了演示迁移流程,我们部署一个极简的存储合约。它只有一个uint256类型的私有状态变量,以及set和get两个函数。set需要花费Gas写入链上,get是只读调用,不消耗AVAX。这个合约虽然简单,但足够覆盖React前端迁移中最常见的读写场景。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleStorage {
uint256 private storedData;
event DataChanged(uint256 newValue);
function set(uint256 x) public {
storedData = x;
emit DataChanged(x);
}
function get() public view returns (uint256) {
return storedData;
}
}
部署可以使用Remix在线IDE,也可以使用Hardhat本地工程。选择Hardhat时,需要在hardhat.config.js里加入Avalanche网络配置,并配置私钥。部署到Fuji测试网后,记录下合约地址。这个地址会作为前端调用的目标。
部署完成后,Remix和Hardhat都会生成ABI文件。ABI本质上是合约接口的JSON描述,React需要它来创建合约实例。你可以把ABI数组导出为SimpleStorage.json,放在React项目的src目录下。以下是一个精简后的ABI片段。
[
{
"inputs": [],
"name": "get",
"outputs": [{"internalType":"uint256","name":"","type":"uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{"internalType":"uint256","name":"x","type":"uint256"}],
"name": "set",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
记住,合约地址与ABI必须配对使用。如果ABI里少了某个函数定义,前端调用就会报错;如果地址写错,交易会发送到错误的合约甚至直接失败。
三、React组件中连接钱包和读取数据
在React组件中连接钱包,核心逻辑是拿到浏览器的ethereum对象,再用ethers.BrowserProvider包装它。BrowserProvider代表只读连接,而签名需要getSigner方法返回的Signer对象。读取链上数据只需要Provider,写入数据才需要Signer。
下面是一个完整的组件示例。它使用useState保存钱包地址和存储值,使用useEffect在钱包连接后自动读取一次合约数据。代码中的SimpleStorageABI从JSON文件导入,CONTRACT_ADDRESS替换为你部署后的地址。
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import Web3Modal from 'web3modal';
import SimpleStorageABI from './SimpleStorage.json';
const CONTRACT_ADDRESS = '0x你的合约地址';
function App() {
const [provider, setProvider] = useState(null);
const [account, setAccount] = useState('');
const [value, setValue] = useState('');
const connectWallet = async () => {
const web3Modal = new Web3Modal({
network: 'avalanche-fuji',
cacheProvider: true
});
const instance = await web3Modal.connect();
const ethersProvider = new ethers.BrowserProvider(instance);
const signer = await ethersProvider.getSigner();
const address = await signer.getAddress();
setProvider(ethersProvider);
setAccount(address);
};
const readValue = async () => {
if (!provider) return;
const contract = new ethers.Contract(CONTRACT_ADDRESS, SimpleStorageABI, provider);
const data = await contract.get();
setValue(data.toString());
};
useEffect(() => {
if (provider) readValue();
}, [provider]);
return (
<div>
<button onClick={connectWallet}>连接钱包</button>
<button onClick={readValue}>读取数据</button>
<p>当前存储值:{value}</p>
</div>
);
}
export default App;
这段代码中,readValue创建合约实例时传入了provider,因此它执行的是只读调用,不会弹出钱包授权窗口。读取到的uint256数据默认是bigint类型,需要调用toString才能在页面中正常显示。
需要注意的是,useEffect的依赖数组里只放了provider,这意味着当钱包连接成功后,读取动作只会执行一次。如果后续有写入交易改变了链上状态,需要手动再次调用readValue刷新界面。
四、发起写入交易与错误处理
写入操作与读取最大的区别在于需要Signer,并且会发起一笔真实的链上交易。用户钱包会弹出确认窗口,要求支付AVAX作为Gas。因此前端必须调用contract.connect(signer)或者直接用Signer构造合约实例,否则交易无法签名。
const writeValue = async (newValue) => {
if (!provider) return;
const signer = await provider.getSigner();
const contract = new ethers.Contract(CONTRACT_ADDRESS, SimpleStorageABI, signer);
const tx = await contract.set(newValue);
await tx.wait();
await readValue();
};
这段代码会把表单中的数字写入合约,然后等待交易确认。等待期间用户需要停留在页面上,不要刷新或关闭,否则可能丢掉交易回执。交易确认后,我们重新读取一次数据,让界面与链上状态保持一致。
实际项目中,交易失败很常见:用户余额不足、网络拥堵导致Gas估算偏低、或者用户在钱包里点了拒绝。以下代码展示了如何捕获错误并做简单降级处理。
try {
const signer = await provider.getSigner();
const contract = new ethers.Contract(CONTRACT_ADDRESS, SimpleStorageABI, signer);
const gasLimit = await contract.set.estimateGas(newValue);
const tx = await contract.set(newValue, { gasLimit });
await tx.wait();
await readValue();
} catch (error) {
if (error.code === 'ACTION_REJECTED') {
console.log('用户取消了交易');
} else {
console.error('交易失败', error);
}
}
除了手动捕获错误,还可以监听钱包的账户切换和网络切换事件。window.ethereum提供了accountsChanged和chainChanged事件,当用户切换地址或链时,前端可以自动刷新页面或者提示重新连接。
if (window.ethereum) {
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
setAccount('');
} else {
setAccount(accounts[0]);
}
});
window.ethereum.on('chainChanged', () => {
window.location.reload();
});
}
把错误处理和事件监听放进同一个useEffect,可以保证在组件挂载时绑定、卸载时清理,避免重复监听导致内存泄漏。
五、抽离合约交互逻辑,保持组件干净
当合约读写逻辑越来越多,直接在组件里维护provider、signer、合约实例会让代码快速膨胀。更合适的做法是把合约交互抽成一个自定义Hook,组件只负责渲染和调用返回值。
import { useMemo, useCallback } from 'react';
import { ethers } from 'ethers';
import SimpleStorageABI from './SimpleStorage.json';
const CONTRACT_ADDRESS = '0x你的合约地址';
function useSimpleStorage(provider) {
const contract = useMemo(() => {
if (!provider) return null;
return new ethers.Contract(CONTRACT_ADDRESS, SimpleStorageABI, provider);
}, [provider]);
const read = useCallback(async () => {
if (!contract) return '0';
const data = await contract.get();
return data.toString();
}, [contract]);
const write = useCallback(async (value) => {
if (!contract) return;
const signer = await provider.getSigner();
const withSigner = contract.connect(signer);
const tx = await withSigner.set(value);
return await tx.wait();
}, [contract, provider]);
return { read, write };
}
这个Hook接收provider作为参数,内部用useMemo缓存合约实例,避免每次渲染都重新创建。读取函数返回字符串,写入函数返回交易回执。组件拿到这些方法后,只需要处理加载状态和错误提示。
经过网络切换、合约部署、ABI接入、组件读写和Hook抽离,原来的React应用已经完整运行在Avalanche链上。普通的表单、按钮、状态管理都没有被破坏,只是数据源从后端API换成了智能合约。后续如果要增加新的合约函数,只需扩展这个Hook并更新ABI即可。
React应用迁移AvalancheSolidity智能合约修改时间:2026-09-26 19:46:12