前端状态管理
核心知识点讲解
状态管理的重要性
在Web3应用中,状态管理尤为重要:
- 复杂状态:Web3应用需要管理钱包状态、合约状态、交易状态等多种状态
- 异步操作:与区块链交互涉及大量异步操作
- 状态共享:多个组件需要共享相同的状态
- 状态持久化:需要在页面刷新后保持状态
- 可预测性:确保状态变化的可预测性和可追踪性
常见的状态管理方案
1. Context API + useReducer
- 内置解决方案:React内置的状态管理方案
- 轻量级:适合中小型应用
- 易于使用:学习曲线平缓
- 灵活性:可以根据需要自定义
2. Redux
- 成熟方案:广泛使用的状态管理库
- 可预测性:严格的单向数据流
- 中间件:支持异步操作和副作用
- 工具生态:丰富的开发工具和中间件
3. Zustand
- 轻量级:比Redux更轻量
- 简单API:使用更简单的API
- 无需Provider:不需要包装应用
- 中间件支持:支持中间件和持久化
4. Recoil
- 原子化状态:基于原子的状态管理
- 派生状态:支持派生状态和选择器
- React集成:与React Hooks深度集成
- 性能优化:自动优化渲染
Web3特定的状态管理考虑
- 钱包状态:连接状态、地址、余额等
- 网络状态:当前网络、网络切换等
- 合约状态:合约数据、交易状态等
- 交易状态:交易提交、确认、失败等
- 缓存策略:缓存区块链数据以提高性能
实用案例分析
Context API实现
// Web3Context.js
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { ethers } from 'ethers';
// 初始状态
const initialState = {
provider: null,
signer: null,
address: '',
balance: '0',
network: null,
isConnected: false,
loading: false,
error: ''
};
// Action类型
const ActionTypes = {
SET_LOADING: 'SET_LOADING',
SET_ERROR: 'SET_ERROR',
SET_PROVIDER: 'SET_PROVIDER',
SET_SIGNER: 'SET_SIGNER',
SET_ADDRESS: 'SET_ADDRESS',
SET_BALANCE: 'SET_BALANCE',
SET_NETWORK: 'SET_NETWORK',
SET_CONNECTED: 'SET_CONNECTED',
RESET: 'RESET'
};
// Reducer
function web3Reducer(state, action) {
switch (action.type) {
case ActionTypes.SET_LOADING:
return { ...state, loading: action.payload };
case ActionTypes.SET_ERROR:
return { ...state, error: action.payload };
case ActionTypes.SET_PROVIDER:
return { ...state, provider: action.payload };
case ActionTypes.SET_SIGNER:
return { ...state, signer: action.payload };
case ActionTypes.SET_ADDRESS:
return { ...state, address: action.payload };
case ActionTypes.SET_BALANCE:
return { ...state, balance: action.payload };
case ActionTypes.SET_NETWORK:
return { ...state, network: action.payload };
case ActionTypes.SET_CONNECTED:
return { ...state, isConnected: action.payload };
case ActionTypes.RESET:
return initialState;
default:
return state;
}
}
// 创建Context
const Web3Context = createContext();
// Provider组件
export function Web3Provider({ children }) {
const [state, dispatch] = useReducer(web3Reducer, initialState);
// 连接钱包
const connectWallet = async () => {
try {
dispatch({ type: ActionTypes.SET_LOADING, payload: true });
dispatch({ type: ActionTypes.SET_ERROR, payload: '' });
if (!window.ethereum) {
throw new Error('MetaMask not detected');
}
// 请求账户访问
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const address = accounts[0];
// 创建Provider和Signer
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// 获取网络信息
const network = await provider.getNetwork();
// 获取余额
const balance = await provider.getBalance(address);
const balanceInEther = ethers.formatEther(balance);
dispatch({ type: ActionTypes.SET_PROVIDER, payload: provider });
dispatch({ type: ActionTypes.SET_SIGNER, payload: signer });
dispatch({ type: ActionTypes.SET_ADDRESS, payload: address });
dispatch({ type: ActionTypes.SET_BALANCE, payload: balanceInEther });
dispatch({ type: ActionTypes.SET_NETWORK, payload: network });
dispatch({ type: ActionTypes.SET_CONNECTED, payload: true });
} catch (err) {
dispatch({ type: ActionTypes.SET_ERROR, payload: 'Failed to connect wallet: ' + err.message });
console.error('Wallet connection error:', err);
} finally {
dispatch({ type: ActionTypes.SET_LOADING, payload: false });
}
};
// 断开钱包
const disconnectWallet = () => {
dispatch({ type: ActionTypes.RESET });
};
// 监听账户变化
useEffect(() => {
if (window.ethereum) {
const handleAccountsChanged = (accounts) => {
if (accounts.length === 0) {
disconnectWallet();
} else {
dispatch({ type: ActionTypes.SET_ADDRESS, payload: accounts[0] });
}
};
const handleNetworkChanged = async (networkId) => {
if (state.provider) {
try {
const network = await state.provider.getNetwork();
dispatch({ type: ActionTypes.SET_NETWORK, payload: network });
} catch (err) {
console.error('Error getting network:', err);
}
}
};
window.ethereum.on('accountsChanged', handleAccountsChanged);
window.ethereum.on('networkChanged', handleNetworkChanged);
return () => {
window.ethereum.off('accountsChanged', handleAccountsChanged);
window.ethereum.off('networkChanged', handleNetworkChanged);
};
}
}, [state.provider]);
const value = {
...state,
connectWallet,
disconnectWallet
};
return (
<Web3Context.Provider value={value}>
{children}
</Web3Context.Provider>
);
}
// 自定义Hook
export function useWeb3() {
const context = useContext(Web3Context);
if (!context) {
throw new Error('useWeb3 must be used within a Web3Provider');
}
return context;
}Redux实现
// store.js
import { configureStore, createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { ethers } from 'ethers';
// 初始状态
const initialState = {
provider: null,
signer: null,
address: '',
balance: '0',
network: null,
isConnected: false,
loading: false,
error: ''
};
// 异步Thunk
export const connectWallet = createAsyncThunk(
'web3/connectWallet',
async (_, { rejectWithValue }) => {
try {
if (!window.ethereum) {
throw new Error('MetaMask not detected');
}
// 请求账户访问
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const address = accounts[0];
// 创建Provider和Signer
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// 获取网络信息
const network = await provider.getNetwork();
// 获取余额
const balance = await provider.getBalance(address);
const balanceInEther = ethers.formatEther(balance);
return {
provider,
signer,
address,
balance: balanceInEther,
network
};
} catch (error) {
return rejectWithValue('Failed to connect wallet: ' + error.message);
}
}
);
// Slice
const web3Slice = createSlice({
name: 'web3',
initialState,
reducers: {
setAddress: (state, action) => {
state.address = action.payload;
},
setNetwork: (state, action) => {
state.network = action.payload;
},
disconnectWallet: (state) => {
return initialState;
}
},
extraReducers: (builder) => {
builder
.addCase(connectWallet.pending, (state) => {
state.loading = true;
state.error = '';
})
.addCase(connectWallet.fulfilled, (state, action) => {
state.provider = action.payload.provider;
state.signer = action.payload.signer;
state.address = action.payload.address;
state.balance = action.payload.balance;
state.network = action.payload.network;
state.isConnected = true;
state.loading = false;
state.error = '';
})
.addCase(connectWallet.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
});
export const { setAddress, setNetwork, disconnectWallet } = web3Slice.actions;
export const selectWeb3 = (state) => state.web3;
export const store = configureStore({
reducer: {
web3: web3Slice.reducer
}
});
// 监听钱包事件
if (typeof window !== 'undefined' && window.ethereum) {
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
store.dispatch(disconnectWallet());
} else {
store.dispatch(setAddress(accounts[0]));
}
});
window.ethereum.on('networkChanged', async (networkId) => {
const state = store.getState();
if (state.web3.provider) {
try {
const network = await state.web3.provider.getNetwork();
store.dispatch(setNetwork(network));
} catch (err) {
console.error('Error getting network:', err);
}
}
});
}Zustand实现
// useWeb3Store.js
import create from 'zustand';
import { persist } from 'zustand/middleware';
import { ethers } from 'ethers';
const useWeb3Store = create(
persist(
(set, get) => ({
// 状态
provider: null,
signer: null,
address: '',
balance: '0',
network: null,
isConnected: false,
loading: false,
error: '',
// 操作
connectWallet: async () => {
try {
set({ loading: true, error: '' });
if (!window.ethereum) {
throw new Error('MetaMask not detected');
}
// 请求账户访问
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const address = accounts[0];
// 创建Provider和Signer
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// 获取网络信息
const network = await provider.getNetwork();
// 获取余额
const balance = await provider.getBalance(address);
const balanceInEther = ethers.formatEther(balance);
set({
provider,
signer,
address,
balance: balanceInEther,
network,
isConnected: true,
loading: false,
error: ''
});
} catch (error) {
set({
error: 'Failed to connect wallet: ' + error.message,
loading: false
});
console.error('Wallet connection error:', error);
}
},
disconnectWallet: () => {
set({
provider: null,
signer: null,
address: '',
balance: '0',
network: null,
isConnected: false,
loading: false,
error: ''
});
},
updateAddress: (address) => {
set({ address });
},
updateNetwork: (network) => {
set({ network });
},
updateBalance: async () => {
const { provider, address } = get();
if (provider && address) {
try {
const balance = await provider.getBalance(address);
const balanceInEther = ethers.formatEther(balance);
set({ balance: balanceInEther });
} catch (error) {
console.error('Error updating balance:', error);
}
}
}
}),
{
name: 'web3-storage', // localStorage key
getStorage: () => localStorage,
partialize: (state) => ({
address: state.address,
network: state.network
})
}
)
);
// 监听钱包事件
if (typeof window !== 'undefined' && window.ethereum) {
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
useWeb3Store.getState().disconnectWallet();
} else {
useWeb3Store.getState().updateAddress(accounts[0]);
useWeb3Store.getState().updateBalance();
}
});
window.ethereum.on('networkChanged', async (networkId) => {
const state = useWeb3Store.getState();
if (state.provider) {
try {
const network = await state.provider.getNetwork();
useWeb3Store.getState().updateNetwork(network);
} catch (err) {
console.error('Error getting network:', err);
}
}
});
}
export default useWeb3Store;合约状态管理
// useContractStore.js
import create from 'zustand';
import { ethers } from 'ethers';
// 合约ABI
const contractABI = [
{
"inputs": [{"internalType": "uint256", "name": "_value", "type": "uint256"}],
"name": "setValue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getValue",
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [{"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"}],
"name": "ValueChanged",
"type": "event"
}
];
// 合约地址
const contractAddress = '0x1234567890123456789012345678901234567890';
const useContractStore = create((set, get) => ({
// 状态
contract: null,
value: '0',
loading: false,
error: '',
transactionHash: '',
// 初始化合约
initContract: (signer) => {
if (signer) {
const contract = new ethers.Contract(contractAddress, contractABI, signer);
set({ contract });
get().loadValue();
get().setupEventListeners();
}
},
// 加载合约值
loadValue: async () => {
const { contract } = get();
if (contract) {
try {
set({ loading: true, error: '' });
const value = await contract.getValue();
set({ value: value.toString(), loading: false, error: '' });
} catch (error) {
set({ error: 'Failed to load value: ' + error.message, loading: false });
console.error('Load value error:', error);
}
}
},
// 设置合约值
setValue: async (newValue) => {
const { contract } = get();
if (contract) {
try {
set({ loading: true, error: '', transactionHash: '' });
const tx = await contract.setValue(newValue);
set({ transactionHash: tx.hash });
await tx.wait();
set({ loading: false, error: '' });
} catch (error) {
set({ error: 'Failed to set value: ' + error.message, loading: false });
console.error('Set value error:', error);
}
}
},
// 设置事件监听器
setupEventListeners: () => {
const { contract } = get();
if (contract) {
contract.on('ValueChanged', (newValue) => {
set({ value: newValue.toString() });
});
}
},
// 重置状态
reset: () => {
set({
contract: null,
value: '0',
loading: false,
error: '',
transactionHash: ''
});
}
}));
export default useContractStore;实践练习
实现Context API状态管理:
- 创建Web3Context
- 实现钱包连接和状态管理
- 在多个组件中使用状态
- 处理错误和加载状态
实现Redux状态管理:
- 创建Redux store
- 实现异步Thunk
- 使用useSelector和useDispatch
- 处理钱包事件
实现Zustand状态管理:
- 创建Zustand store
- 实现持久化
- 处理异步操作
- 与React组件集成
实现合约状态管理:
- 管理合约实例
- 处理合约方法调用
- 监听合约事件
- 管理交易状态
性能优化:
- 使用memo和useMemo
- 优化渲染
- 合理缓存状态
- 减少不必要的重渲染
总结
前端状态管理是Web3应用开发的重要组成部分,它使得应用能够有效地管理复杂的状态和异步操作。通过本集的学习,你应该能够:
- 选择合适的状态管理方案:根据应用规模和复杂度选择Context API、Redux或Zustand
- 管理钱包状态:处理钱包连接、地址、余额等状态
- 管理合约状态:处理合约实例、方法调用、事件监听等
- 处理异步操作:管理异步操作的状态和错误
- 优化性能:减少不必要的重渲染,提高应用性能
在实际开发中,状态管理方案的选择应该根据应用的具体需求和规模来决定。对于小型应用,Context API可能已经足够;对于大型应用,Redux或Zustand可能更为合适。
无论选择哪种方案,都应该注重状态的可预测性、可维护性和性能,以创建出用户体验良好的Web3应用。