引言
DApp开发与传统Web应用最大的区别在于数据的来源。传统应用从后端API获取数据,而DApp需要从区块链节点同步状态。这种数据获取方式的根本差异,使得状态管理成为DApp前端开发中最具挑战性的问题之一。
本文将系统性地探讨DApp前端的状态管理策略,从基础的状态抽象到高级的缓存优化,帮助你构建响应迅速、用户体验优秀的去中心化应用。

一、DApp状态管理的核心挑战
1.1 数据获取的特性
区块链数据的获取与传统API有本质区别:
javascript
// 传统API vs 区块链数据的对比
// 传统API
const response = await fetch('/api/user/balance');
const balance = response.json(); // 瞬时返回
// 区块链数据
const balance = await contract.balanceOf(userAddress);
// 需要:
// 1. 构造交易或调用
// 2. 发送到节点
// 3. 等待响应
// 4. 解析返回数据
// 可能需要数秒到数十秒
1.2 需要管理的多种状态
typescript
// DApp中需要管理的各种状态类型
// 1. 链上状态 - 存储在智能合约中
interface OnChainState {
// Token余额
tokenBalance: bigint;
// NFT所有权
ownedNfts: string[];
// 投票权重
votingPower: bigint;
}
// 2. 交易状态 - pending/confirmed/failed
interface TransactionState {
status: 'idle' | 'pending' | 'confirming' | 'confirmed' | 'failed';
hash?: string;
confirmations: number;
receipt?: TransactionReceipt;
error?: Error;
}
// 3. 合约元数据 - ABI、地址等
interface ContractMeta {
address: Address;
abi: ABI;
chainId: number;
}
// 4. 用户界面状态
interface UIState {
selectedToken: Address | null;
isModalOpen: boolean;
activeTab: 'swap' | 'pool' | 'stats';
theme: 'light' | 'dark';
}
// 5. Web3连接状态
interface Web3State {
address: Address | null;
chainId: number | null;
isConnecting: boolean;
provider: BrowserProvider | null;
}
1.3 状态管理的目标
typescript
// 理想的状态管理应该满足:
// 1. 单一数据源 - Single Source of Truth
// 2. 可预测的状态变化 - Predictable Updates
// 3. 高效的重新渲染 - Efficient Re-renders
// 4. 持久化关键状态 - Persistence
// 5. 错误恢复能力 - Error Recovery
interface StateManagementGoals {
singleSourceOfTruth: true; // 所有状态从单一store管理
predictableUpdates: true; // 状态变化可追踪、可回滚
efficientRenders: true; // 只在需要时重新渲染
persistence: true; // 刷新页面不丢失关键状态
errorRecovery: true; // 错误后能恢复到正确状态
}
二、React状态管理方案
2.1 Context + Hooks 基础方案
typescript
// 基础Web3 Context实现
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { BrowserProvider, JsonRpcSigner } from 'ethers';
// 定义Context类型
interface Web3ContextType {
address: Address | null;
chainId: number | null;
provider: BrowserProvider | null;
signer: JsonRpcSigner | null;
connect: () => Promise<void>;
disconnect: () => void;
isConnecting: boolean;
error: Error | null;
}
// 创建Context
const Web3Context = createContext<Web3ContextType | null>(null);
// Provider组件
export function Web3Provider({ children }: { children: ReactNode }) {
const [address, setAddress] = useState<Address | null>(null);
const [chainId, setChainId] = useState<number | null>(null);
const [provider, setProvider] = useState<BrowserProvider | null>(null);
const [signer, setSigner] = useState<JsonRpcSigner | null>(null);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<Error | null>(null);
// 监听账户变化
useEffect(() => {
if (!provider) return;
const handleAccountsChanged = (accounts: Address[]) => {
if (accounts.length === 0) {
disconnect();
} else {
setAddress(accounts[0]);
}
};
const handleChainChanged = (chainIdHex: string) => {
setChainId(parseInt(chainIdHex, 16));
// 刷新页面以加载新链数据
window.location.reload();
};
provider.on('accountsChanged', handleAccountsChanged);
provider.on('chainChanged', handleChainChanged);
return () => {
provider.removeListener('accountsChanged', handleAccountsChanged);
provider.removeListener('chainChanged', handleChainChanged);
};
}, [provider]);
const connect = async () => {
setIsConnecting(true);
setError(null);
try {
// 检查MetaMask是否存在
if (!window.ethereum) {
throw new Error('Please install MetaMask to use this DApp');
}
// 请求账户连接
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
}) as Address[];
const browserProvider = new BrowserProvider(window.ethereum);
const browserSigner = await browserProvider.getSigner();
const network = await browserProvider.getNetwork();
setProvider(browserProvider);
setSigner(browserSigner);
setAddress(accounts[0]);
setChainId(Number(network.chainId));
} catch (err) {
setError(err as Error);
console.error('Failed to connect wallet:', err);
} finally {
setIsConnecting(false);
}
};
const disconnect = () => {
setAddress(null);
setChainId(null);
setProvider(null);
setSigner(null);
};
return (
<Web3Context.Provider
value={{
address,
chainId,
provider,
signer,
connect,
disconnect,
isConnecting,
error
}}
>
{children}
</Web3Context.Provider>
);
}
// 自定义Hook
export function useWeb3() {
const context = useContext(Web3Context);
if (!context) {
throw new Error('useWeb3 must be used within Web3Provider');
}
return context;
}
2.2 Zustand 状态管理
Zustand是DApp开发中非常流行的状态管理库,它比Redux轻量且使用更简单:
typescript
// 安装:npm install zustand
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { BrowserProvider, JsonRpcSigner } from 'ethers';
// ========== Web3 Store ==========
interface Web3Store {
// State
address: Address | null;
chainId: number | null;
provider: BrowserProvider | null;
signer: JsonRpcSigner | null;
isConnecting: boolean;
error: Error | null;
// Actions
setProvider: (provider: BrowserProvider | null) => void;
setAddress: (address: Address | null) => void;
setChainId: (chainId: number | null) => void;
connect: () => Promise<void>;
disconnect: () => void;
reset: () => void;
}
export const useWeb3Store = create<Web3Store>()((set, get) => ({
address: null,
chainId: null,
provider: null,
signer: null,
isConnecting: false,
error: null,
setProvider: (provider) => set({ provider }),
setAddress: (address) => set({ address }),
setChainId: (chainId) => set({ chainId }),
connect: async () => {
set({ isConnecting: true, error: null });
try {
if (!window.ethereum) {
throw new Error('MetaMask not found');
}
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
}) as Address[];
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const network = await provider.getNetwork();
set({
provider,
signer,
address: accounts[0],
chainId: Number(network.chainId),
isConnecting: false
});
} catch (err) {
set({ error: err as Error, isConnecting: false });
throw err;
}
},
disconnect: () => set({
address: null,
chainId: null,
provider: null,
signer: null,
error: null
}),
reset: () => set({
address: null,
chainId: null,
provider: null,
signer: null,
isConnecting: false,
error: null
})
}));
// ========== Token Balance Store ==========
interface TokenBalance {
balance: bigint;
lastUpdated: number;
isLoading: boolean;
error: Error | null;
}
interface TokenStore {
balances: Record<Address, TokenBalance>;
fetchBalance: (tokenAddress: Address, account: Address) => Promise<void>;
updateBalance: (tokenAddress: Address, balance: bigint) => void;
clearBalances: () => void;
}
export const useTokenStore = create<TokenStore>((set, get) => ({
balances: {},
fetchBalance: async (tokenAddress, account) => {
const tokenId = `${tokenAddress}-${account}`;
set((state) => ({
balances: {
...state.balances,
[tokenId]: {
...state.balances[tokenId],
isLoading: true,
error: null
}
}
}));
try {
// 实际项目中这里会调用合约
const balance = await fetchTokenBalance(tokenAddress, account);
set((state) => ({
balances: {
...state.balances,
[tokenId]: {
balance,
lastUpdated: Date.now(),
isLoading: false,
error: null
}
}
}));
} catch (err) {
set((state) => ({
balances: {
...state.balances,
[tokenId]: {
...state.balances[tokenId],
isLoading: false,
error: err as Error
}
}
}));
}
},
updateBalance: (tokenAddress, balance) => {
const tokenId = `${tokenAddress}-${get().balances}`;
set((state) => ({
balances: {
...state.balances,
[tokenId]: {
balance,
lastUpdated: Date.now(),
isLoading: false,
error: null
}
}
}));
},
clearBalances: () => set({ balances: {} })
}));
// ========== 带持久化的用户偏好Store ==========
interface PreferencesStore {
theme: 'light' | 'dark';
language: 'en' | 'zh';
slippageTolerance: number;
transactionDeadline: number; // 分钟
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (language: 'en' | 'zh') => void;
setSlippageTolerance: (tolerance: number) => void;
setTransactionDeadline: (deadline: number) => void;
}
export const usePreferencesStore = create<PreferencesStore>()(
persist(
(set) => ({
theme: 'dark',
language: 'en',
slippageTolerance: 0.5, // 0.5%
transactionDeadline: 20, // 20分钟
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
setSlippageTolerance: (slippageTolerance) => set({ slippageTolerance }),
setTransactionDeadline: (transactionDeadline) => set({ transactionDeadline })
}),
{
name: 'dapp-preferences',
storage: createJSONStorage(() => localStorage)
}
)
);
2.3 React Query + Web3 集成
TanStack Query(原React Query)是管理异步数据的神器,与Web3数据完美契合:
typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useContract, useContractWrite } from 'wagmi';
import { parseEther, formatEther } from 'viem';
// 合约配置
const USDC_CONTRACT = {
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: [
{
name: 'balanceOf',
type: 'function',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ type: 'uint256' }]
},
{
name: 'transfer',
type: 'function',
inputs: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ type: 'bool' }]
}
]
} as const;
// ========== 查询Hook ==========
export function useTokenBalance(address: Address | undefined) {
return useQuery({
queryKey: ['token-balance', USDC_CONTRACT.address, address],
queryFn: async () => {
if (!address) return null;
// 使用 wagmi 的 useContractRead
const balance = await readContract({
address: USDC_CONTRACT.address,
abi: USDC_CONTRACT.abi,
functionName: 'balanceOf',
args: [address]
});
return {
raw: balance as bigint,
formatted: formatEther(balance as bigint),
decimals: 6 // USDC decimals
};
},
enabled: !!address,
staleTime: 1000 * 30, // 30秒内认为数据新鲜
refetchInterval: 1000 * 60 // 每分钟自动刷新
});
}
// ========== 交易Hook ==========
export function useTokenTransfer() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ to, amount }: { to: Address; amount: bigint }) => {
const hash = await writeContract({
address: USDC_CONTRACT.address,
abi: USDC_CONTRACT.abi,
functionName: 'transfer',
args: [to, amount]
});
return hash;
},
onSuccess: (hash, variables) => {
// 交易发送后,乐观更新UI
queryClient.setQueryData(
['token-balance', USDC_CONTRACT.address, variables.to],
(oldData: { raw: bigint } | undefined) => {
if (!oldData) return oldData;
return {
...oldData,
raw: oldData.raw + variables.amount
};
}
);
// 或者使相关查询失效
queryClient.invalidateQueries({
queryKey: ['token-balance', USDC_CONTRACT.address]
});
}
});
}
// ========== 组合使用 ==========
function TransferForm() {
const { address } = useAccount();
const { data: balance, isLoading } = useTokenBalance(address);
const transferMutation = useTokenTransfer();
const handleTransfer = (to: Address, amount: string) => {
const amountWei = parseEther(amount);
transferMutation.mutate({ to, amount: amountWei });
};
if (isLoading) return <div>Loading...</div>;
return (
<div>
<p>Balance: {balance?.formatted} USDC</p>
<button
onClick={() => handleTransfer('0x...', '100')}
disabled={transferMutation.isPending}
>
{transferMutation.isPending ? 'Transferring...' : 'Transfer'}
</button>
</div>
);
}
三、链上数据实时同步
3.1 事件监听架构
typescript
// 事件监听管理器
class EventListenerManager {
private listeners: Map<string, Set<Listener>> = new Map();
private provider: BrowserProvider;
private chainId: number;
constructor(provider: BrowserProvider) {
this.provider = provider;
this.chainId = provider.getNetwork().then(n => Number(n.chainId));
}
subscribe<T>(
contract: Contract,
eventName: string,
callback: (event: T) => void,
filter?: Filter
) {
const key = `${contract.address}-${eventName}`;
const listener = contract.filters[eventName as keyof typeof contract.filters](
...(filter?.args || [])
) as Listener;
const wrappedCallback = (args: T) => {
try {
callback(args);
} catch (err) {
console.error(`Event callback error for ${eventName}:`, err);
}
};
contract.on(listener, wrappedCallback);
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key)!.add(wrappedCallback);
// 返回取消订阅函数
return () => {
contract.off(listener, wrappedCallback);
this.listeners.get(key)?.delete(wrappedCallback);
};
}
// 批量取消订阅
unsubscribeAll() {
this.listeners.forEach((callbacks) => {
callbacks.clear();
});
this.listeners.clear();
}
}
// ========== React集成 ==========
import { useEffect, useRef, useCallback } from 'react';
export function useContractEvent(
contract: Contract,
eventName: string,
callback: (...args: any[]) => void,
dependencies: any[] = []
) {
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
const listener = (...args: any[]) => {
callbackRef.current(...args);
};
// @ts-ignore
contract.on(eventName, listener);
return () => {
// @ts-ignore
contract.off(eventName, listener);
};
}, [contract, eventName, ...dependencies]);
}
// ========== Transfer事件监听示例 ==========
function TransferListener({ address }: { address: Address }) {
const [transfers, setTransfers] = useState<Transfer[]>([]);
// 假设 tokenContract 已定义
const tokenContract = useTokenContract();
useContractEvent(
tokenContract,
'Transfer',
(from: Address, to: Address, value: bigint) => {
// 只关心涉及当前用户的事件
if (from === address || to === address) {
setTransfers((prev) => [
{ from, to, value, timestamp: Date.now() },
...prev.slice(0, 99) // 保留最近100条
]);
}
},
[address]
);
return (
<div>
<h3>Recent Transfers</h3>
<ul>
{transfers.map((t, i) => (
<li key={i}>
{t.from === address ? 'Sent' : 'Received'} {formatEther(t.value)}
</li>
))}
</ul>
</div>
);
}
3.2 区块头监听
typescript
// 实时区块更新Hook
export function useNewBlock(callback: (blockNumber: number) => void) {
const { provider } = useWeb3Store();
useEffect(() => {
if (!provider) return;
const handleNewBlock = (blockNumber: number) => {
callback(blockNumber);
};
provider.on('block', handleNewBlock);
return () => {
provider.off('block', handleNewBlock);
};
}, [provider, callback]);
}
// ========== 实时余额更新 ==========
function useLiveTokenBalance(tokenAddress: Address, account: Address | undefined) {
const [balance, setBalance] = useState<bigint | null>(null);
const [isLoading, setIsLoading] = useState(true);
// 初始获取
useEffect(() => {
if (!account) {
setBalance(null);
setIsLoading(false);
return;
}
const fetchBalance = async () => {
setIsLoading(true);
try {
const balance = await getTokenBalance(tokenAddress, account);
setBalance(balance);
} catch (err) {
console.error('Failed to fetch balance:', err);
} finally {
setIsLoading(false);
}
};
fetchBalance();
}, [tokenAddress, account]);
// 新区块时刷新
useNewBlock(async () => {
if (!account) return;
try {
const newBalance = await getTokenBalance(tokenAddress, account);
setBalance(newBalance);
} catch (err) {
console.error('Failed to refresh balance:', err);
}
});
return { balance, isLoading };
}
// ========== 确认数更新 ==========
export function useTransactionConfirmations(txHash: Address | undefined) {
const [confirmations, setConfirmations] = useState(0);
const [isConfirmed, setIsConfirmed] = useState(false);
const requiredConfirmations = 12;
const { provider } = useWeb3Store();
useEffect(() => {
if (!txHash || !provider) return;
const checkConfirmations = async () => {
try {
const receipt = await provider.getTransactionReceipt(txHash);
if (!receipt) return;
const currentBlock = await provider.getBlockNumber();
const blockDiff = currentBlock - receipt.blockNumber;
setConfirmations(blockDiff);
setIsConfirmed(blockDiff >= requiredConfirmations);
} catch (err) {
console.error('Failed to check confirmations:', err);
}
};
// 立即检查
checkConfirmations();
// 监听新区块
provider.on('block', checkConfirmations);
return () => {
provider.off('block', checkConfirmations);
};
}, [txHash, provider]);
return { confirmations, isConfirmed };
}
3.3 WebSocket实时数据
typescript
// Alchemy WebSocket Provider配置
import { AlchemyProvider, WebSocketProvider } from 'ethers';
const alchemyApiKey = process.env.ALCHEMY_API_KEY;
const wsProvider = new WebSocketProvider(
`wss://eth-mainnet.g.alchemy.com/v2/${alchemyApiKey}`
);
// ========== 实时价格订阅 ==========
export function useEthPrice() {
const [price, setPrice] = useState<number | null>(null);
const [priceChange, setPriceChange] = useState<number>(0);
useEffect(() => {
let isMounted = true;
const fetchInitialPrice = async () => {
try {
// 使用Alchemy Price Oracle
const response = await fetch(
`https://api.etherscan.io/api?module=stats&action=ethprice`
);
const data = await response.json();
if (isMounted && data.status === '1') {
setPrice(parseFloat(data.result.ethusd));
}
} catch (err) {
console.error('Failed to fetch ETH price:', err);
}
};
// WebSocket订阅价格更新
const subscribeToPrice = async () => {
wsProvider.on('block', async () => {
try {
const response = await fetch(
`https://api.etherscan.io/api?module=stats&action=ethprice`
);
const data = await response.json();
if (isMounted && data.status === '1') {
const newPrice = parseFloat(data.result.ethusd);
setPriceChange(prev => newPrice - price!);
setPrice(newPrice);
}
} catch (err) {
console.error('Failed to update ETH price:', err);
}
});
};
fetchInitialPrice();
subscribeToPrice();
return () => {
isMounted = false;
wsProvider.removeAllListeners('block');
};
}, []);
return { price, priceChange };
}
// ========== 简化版:使用ethers的监听功能 ==========
export function useBlockNumber() {
const [blockNumber, setBlockNumber] = useState<number>(0);
const { provider } = useWeb3Store();
useEffect(() => {
if (!provider) return;
const fetchCurrentBlock = async () => {
const block = await provider.getBlockNumber();
setBlockNumber(block);
};
fetchCurrentBlock();
provider.on('block', setBlockNumber);
return () => {
provider.off('block', setBlockNumber);
};
}, [provider]);
return blockNumber;
}
四、数据缓存与优化
4.1 多层缓存策略
typescript
// 缓存策略配置
interface CacheConfig {
// 缓存有效期(毫秒)
ttl: number;
// 最大缓存条目数
maxSize: number;
// 缓存Key前缀
prefix: string;
}
// 缓存条目
interface CacheEntry<T> {
data: T;
timestamp: number;
hits: number;
}
// 简单内存缓存实现
class Web3Cache {
private cache: Map<string, CacheEntry<any>> = new Map();
private config: CacheConfig;
constructor(config: CacheConfig) {
this.config = config;
}
get<T>(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
// 检查是否过期
if (Date.now() - entry.timestamp > this.config.ttl) {
this.cache.delete(key);
return null;
}
// 更新命中计数
entry.hits++;
return entry.data as T;
}
set<T>(key: string, data: T): void {
// 检查缓存大小
if (this.cache.size >= this.config.maxSize) {
this.evictLeastUsed();
}
this.cache.set(key, {
data,
timestamp: Date.now(),
hits: 0
});
}
private evictLeastUsed(): void {
let minHits = Infinity;
let minKey: string | null = null;
for (const [key, entry] of this.cache) {
if (entry.hits < minHits) {
minHits = entry.hits;
minKey = key;
}
}
if (minKey) {
this.cache.delete(minKey);
}
}
clear(): void {
this.cache.clear();
}
}
// 创建缓存实例
const web3Cache = new Web3Cache({
ttl: 1000 * 60, // 1分钟
maxSize: 100,
prefix: 'web3'
});
// ========== 带缓存的数据获取 ==========
export async function getCachedTokenBalance(
tokenAddress: Address,
account: Address
): Promise<bigint> {
const cacheKey = `balance-${tokenAddress}-${account}`;
// 先检查缓存
const cached = web3Cache.get<bigint>(cacheKey);
if (cached !== null) {
console.log('Using cached balance');
return cached;
}
// 缓存未命中,从链上获取
const balance = await fetchTokenBalance(tokenAddress, account);
// 更新缓存
web3Cache.set(cacheKey, balance);
return balance;
}
4.2 乐观更新模式
typescript
// 乐观更新Hook
export function useOptimisticUpdate<T>() {
const [optimisticData, setOptimisticData] = useState<T | null>(null);
const [isOptimistic, setIsOptimistic] = useState(false);
const applyOptimisticUpdate = useCallback((
updater: (current: T | null) => T,
onConfirm: (newData: T) => void,
onRollback: () => void
) => {
// 应用乐观更新
setOptimisticData(prev => updater(prev));
setIsOptimistic(true);
// 返回确认/回滚函数
return {
confirm: (confirmedData: T) => {
setOptimisticData(confirmedData);
setIsOptimistic(false);
onConfirm(confirmedData);
},
rollback: () => {
setOptimisticData(null);
setIsOptimistic(false);
onRollback();
}
};
}, []);
return { optimisticData, isOptimistic, applyOptimisticUpdate };
}
// ========== 乐观更新示例:Like功能 ==========
function useLikes(postId: string) {
const [likes, setLikes] = useState<Set<Address>>(new Set());
const { address } = useWeb3Store();
// 初始加载
useEffect(() => {
fetchLikes(postId).then(setLikes);
}, [postId]);
const toggleLike = async () => {
if (!address) return;
const isLiked = likes.has(address);
const newLikes = new Set(likes);
// 乐观更新
if (isLiked) {
newLikes.delete(address);
} else {
newLikes.add(address);
}
setLikes(newLikes);
try {
// 发送交易
await sendLikeTransaction(postId);
// 交易确认后,可能需要从链上重新同步
// 但对于Like这种场景,乐观更新通常已经足够
} catch (err) {
// 失败,回滚
setLikes(likes);
console.error('Failed to toggle like:', err);
}
};
return { likes, toggleLike, isLiked: likes.has(address) };
}
// ========== 交易历史乐观更新 ==========
function useTransactionHistory() {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const { address } = useWeb3Store();
const queryClient = useQueryClient();
// 加载历史交易
const { data: history = [] } = useQuery({
queryKey: ['tx-history', address],
queryFn: () => fetchTransactionHistory(address),
enabled: !!address
});
useEffect(() => {
setTransactions(history);
}, [history]);
const sendTransaction = async (tx: PendingTransaction) => {
// 创建临时交易
const tempTx: Transaction = {
...tx,
id: `temp-${Date.now()}`,
status: 'pending',
hash: null
};
// 乐观添加到列表开头
setTransactions(prev => [tempTx, ...prev]);
try {
// 发送交易
const hash = await executeTransaction(tx);
// 更新临时交易
setTransactions(prev =>
prev.map(t =>
t.id === tempTx.id
? { ...t, hash, status: 'pending' }
: t
)
);
// 等待确认
const receipt = await waitForTransaction(hash);
// 更新最终状态
setTransactions(prev =>
prev.map(t =>
t.hash === hash
? { ...t, status: 'confirmed', receipt }
: t
)
);
} catch (err) {
// 失败,标记为失败
setTransactions(prev =>
prev.map(t =>
t.id === tempTx.id
? { ...t, status: 'failed', error: err as Error }
: t
)
);
// 一定时间后从列表移除失败交易
setTimeout(() => {
setTransactions(prev => prev.filter(t => t.id !== tempTx.id));
}, 5000);
}
};
return { transactions, sendTransaction };
}
4.3 数据分页与虚拟化
typescript
// ========== 分页加载示例 ==========
interface PaginatedResult<T> {
items: T[];
total: number;
hasMore: boolean;
nextCursor?: string;
}
async function fetchNFTs(
address: Address,
cursor?: string,
pageSize: number = 20
): Promise<PaginatedResult<NFT>> {
// 使用Alchemy NFT API
const response = await fetch(
`https://eth-mainnet.g.alchemy.com/nft/v3/${ALCHEMY_KEY}/getNFTsForOwner`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
owner: address,
pageKey: cursor,
pageSize,
withMetadata: true
})
}
);
const data = await response.json();
return {
items: data.ownedNfts,
total: data.totalCount,
hasMore: !!data.pageKey,
nextCursor: data.pageKey
};
}
// ========== 无限滚动Hook ==========
export function useInfiniteNFTs(address: Address | undefined) {
const [nfts, setNfts] = useState<NFT[]>([]);
const [cursor, setCursor] = useState<string | undefined>();
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const observerRef = useRef<IntersectionObserver>();
const loadMore = useCallback(async () => {
if (!address || isLoading || !hasMore) return;
setIsLoading(true);
try {
const result = await fetchNFTs(address, cursor);
setNfts(prev => [...prev, ...result.items]);
setCursor(result.nextCursor);
setHasMore(result.hasMore);
} catch (err) {
console.error('Failed to load NFTs:', err);
} finally {
setIsLoading(false);
}
}, [address, cursor, hasMore, isLoading]);
// 初始加载
useEffect(() => {
if (address) {
setNfts([]);
setCursor(undefined);
setHasMore(true);
loadMore();
}
}, [address]);
// 无限滚动触发器
const loadMoreRef = useCallback((node: HTMLElement | null) => {
if (observerRef.current) {
observerRef.current.disconnect();
}
if (!node) return;
observerRef.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore && !isLoading) {
loadMore();
}
});
observerRef.current.observe(node);
}, [loadMore, hasMore, isLoading]);
return { nfts, loadMoreRef, isLoading, hasMore };
}
// ========== 虚拟化长列表 ==========
import { useVirtualizer } from '@tanstack/react-virtual';
function NFTGallery({ nfts }: { nfts: NFT[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: nfts.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 300,
overscan: 5
});
return (
<div
ref={parentRef}
style={{ height: '600px', overflow: 'auto' }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative'
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`
}}
>
<NFTCard nft={nfts[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
五、完整应用示例
5.1 Web3状态管理架构
typescript
// ========== 完整的状态管理架构 ==========
// stores/web3Store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
// 合约Store
interface ContractStore {
addresses: Record<string, Address>;
abis: Record<string, ABI>;
setContractAddress: (name: string, address: Address) => void;
getContract: (name: string) => ContractConfig | null;
}
export const useContractStore = create<ContractStore>()(
persist(
(set, get) => ({
addresses: {},
abis: {},
setContractAddress: (name, address) =>
set(state => ({
addresses: { ...state.addresses, [name]: address }
})),
getContract: (name) => {
const { addresses, abis } = get();
const address = addresses[name];
const abi = abis[name];
if (!address || !abi) return null;
return { address, abi };
}
}),
{
name: 'contracts-config',
storage: createJSONStorage(() => localStorage)
}
)
);
// 交易Store
interface TransactionStore {
pendingTxs: PendingTransaction[];
addPendingTx: (tx: PendingTransaction) => void;
updateTx: (id: string, updates: Partial<PendingTransaction>) => void;
removeTx: (id: string) => void;
clearAll: () => void;
}
interface PendingTransaction {
id: string;
type: string;
status: 'pending' | 'confirming' | 'confirmed' | 'failed';
hash?: Address;
confirmations: number;
timestamp: number;
description: string;
params?: Record<string, any>;
}
export const useTransactionStore = create<TransactionStore>((set) => ({
pendingTxs: [],
addPendingTx: (tx) =>
set(state => ({
pendingTxs: [tx, ...state.pendingTxs].slice(0, 50) // 最多保留50条
})),
updateTx: (id, updates) =>
set(state => ({
pendingTxs: state.pendingTxs.map(tx =>
tx.id === id ? { ...tx, ...updates } : tx
)
})),
removeTx: (id) =>
set(state => ({
pendingTxs: state.pendingTxs.filter(tx => tx.id !== id)
})),
clearAll: () => set({ pendingTxs: [] })
}));
5.2 应用入口整合
typescript
// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WagmiProvider, createConfig, http } from 'wagmi';
import { mainnet, polygon, arbitrum } from 'wagmi/chains';
import { MetaMaskConnector } from 'wagmi/connectors/metaMask';
import { Web3Provider } from './providers/Web3Provider';
import { TransactionProvider } from './providers/TransactionProvider';
// Wagmi配置
const wagmiConfig = createConfig({
chains: [mainnet, polygon, arbitrum],
connectors: [
new MetaMaskConnector()
],
transports: {
[mainnet.id]: http(),
[polygon.id]: http(),
[arbitrum.id]: http()
}
});
// React Query配置
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 30, // 30秒
gcTime: 1000 * 60 * 5, // 5分钟
retry: 2,
refetchOnWindowFocus: false
}
}
});
function App() {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<Web3Provider>
<TransactionProvider>
<MainLayout>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/swap" element={<Swap />} />
<Route path="/pool" element={<Pool />} />
<Route path="/nft" element={<NFTGallery />} />
</Routes>
</MainLayout>
</TransactionProvider>
</Web3Provider>
</QueryClientProvider>
</WagmiProvider>
);
}
总结
DApp前端的状态管理是一个复杂的系统工程,需要综合考虑多个维度:
| 维度 | 关键点 | 推荐方案 |
|---|---|---|
| Web3连接 | 钱包状态、链切换 | Zustand + Context |
| 链上数据 | 实时同步、缓存 | React Query + 事件监听 |
| 交易状态 | pending/确认/失败 | 独立Transaction Store |
| UI状态 | 主题、模态框 | Zustand (持久化) |
| 性能 | 虚拟化、乐观更新 | TanStack Virtual |
核心原则:
- 分层管理 – Web3状态、业务状态、UI状态分而治之
- 实时感知 – 通过事件监听保持链上数据同步
- 智能缓存 – 多层缓存策略减少不必要的链上查询
- 乐观更新 – 提升用户体验,交易即反馈
- 错误恢复 – 完善的错误处理和状态回滚机制
相关推荐:

发表回复