DApp前端数据管理最佳实践:状态管理与实时链上数据同步

DApp前端数据管理最佳实践封面,状态管理与链上链下数据同步可视化

引言

DApp开发与传统Web应用最大的区别在于数据的来源。传统应用从后端API获取数据,而DApp需要从区块链节点同步状态。这种数据获取方式的根本差异,使得状态管理成为DApp前端开发中最具挑战性的问题之一。

本文将系统性地探讨DApp前端的状态管理策略,从基础的状态抽象到高级的缓存优化,帮助你构建响应迅速、用户体验优秀的去中心化应用。

DApp状态管理方案对比图,Context+Hooks、Zustand、React Query三大方案与实时同步策略

一、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

核心原则

  1. 分层管理 – Web3状态、业务状态、UI状态分而治之
  2. 实时感知 – 通过事件监听保持链上数据同步
  3. 智能缓存 – 多层缓存策略减少不必要的链上查询
  4. 乐观更新 – 提升用户体验,交易即反馈
  5. 错误恢复 – 完善的错误处理和状态回滚机制

相关推荐

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注