前端状态管理

核心知识点讲解

状态管理的重要性

在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;

实践练习

  1. 实现Context API状态管理

    • 创建Web3Context
    • 实现钱包连接和状态管理
    • 在多个组件中使用状态
    • 处理错误和加载状态
  2. 实现Redux状态管理

    • 创建Redux store
    • 实现异步Thunk
    • 使用useSelector和useDispatch
    • 处理钱包事件
  3. 实现Zustand状态管理

    • 创建Zustand store
    • 实现持久化
    • 处理异步操作
    • 与React组件集成
  4. 实现合约状态管理

    • 管理合约实例
    • 处理合约方法调用
    • 监听合约事件
    • 管理交易状态
  5. 性能优化

    • 使用memo和useMemo
    • 优化渲染
    • 合理缓存状态
    • 减少不必要的重渲染

总结

前端状态管理是Web3应用开发的重要组成部分,它使得应用能够有效地管理复杂的状态和异步操作。通过本集的学习,你应该能够:

  • 选择合适的状态管理方案:根据应用规模和复杂度选择Context API、Redux或Zustand
  • 管理钱包状态:处理钱包连接、地址、余额等状态
  • 管理合约状态:处理合约实例、方法调用、事件监听等
  • 处理异步操作:管理异步操作的状态和错误
  • 优化性能:减少不必要的重渲染,提高应用性能

在实际开发中,状态管理方案的选择应该根据应用的具体需求和规模来决定。对于小型应用,Context API可能已经足够;对于大型应用,Redux或Zustand可能更为合适。

无论选择哪种方案,都应该注重状态的可预测性、可维护性和性能,以创建出用户体验良好的Web3应用。

« 上一篇 钱包连接与认证 下一篇 » 前端UI组件库