Files
2026-01-25 15:05:03 +08:00

21 KiB
Raw Permalink Blame History

状态管理设计文档

1. 状态管理方案选择

1.1 为什么选择Context API

对于本项目的需求,React Context API是最佳选择:

优势:

  • 轻量级,无需引入额外的库
  • React官方内置,学习成本低
  • 适合中小型应用的状态管理
  • 与React生态系统完美集成

适用场景:

  • 认证状态(用户信息、Token
  • 全局配置(主题、语言)
  • 简单的共享状态

不适用场景:

  • 复杂的跨组件通信
  • 高频率的状态更新
  • 需要时间旅行调试
  • 如果将来需要,可以迁移到Redux或Zustand

1.2 状态管理架构

┌─────────────────────────────────────────┐
│           App Root                        │
├─────────────────────────────────────────┤
│  AuthProvider (认证状态)                  │
│  ├─ user (用户信息)                      │
│  ├─ token (认证令牌)                     │
│  └─ authState (认证状态)                │
│                                          │
│  ThemeProvider (主题状态,可选)          │
│  └─ theme (主题配置)                     │
│                                          │
│  Router (路由状态)                       │
│  └─ currentPath (当前路径)              │
└─────────────────────────────────────────┘

2. Context实现

2.1 AuthContext(认证上下文)

// contexts/AuthContext.jsx
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { authAPI } from '../services/auth';
import { message } from 'antd';

const AuthContext = createContext(null);

/**
 * 认证上下文Provider
 */
export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(null);
  const [loading, setLoading] = useState(true);
  const [authState, setAuthState] = useState({
    isAuthenticated: false,
    isLoading: true,
  });

  /**
   * 初始化认证状态
   */
  useEffect(() => {
    checkAuth();
  }, []);

  /**
   * 检查登录状态
   */
  const checkAuth = async () => {
    try {
      const savedToken = localStorage.getItem('token');
      const savedUser = localStorage.getItem('user');

      if (savedToken && savedUser) {
        // 验证Token有效性
        const currentUser = await authAPI.getCurrentUser();

        setToken(savedToken);
        setUser(currentUser);
        setAuthState({
          isAuthenticated: true,
          isLoading: false,
        });
      } else {
        clearAuth();
      }
    } catch (error) {
      console.error('Auth check failed:', error);
      clearAuth();
    } finally {
      setLoading(false);
    }
  };

  /**
   * 登录
   */
  const login = async (username, password) => {
    try {
      const response = await authAPI.login(username, password);

      const { token: newToken, user: userData } = response;

      // 保存到localStorage
      localStorage.setItem('token', newToken);
      localStorage.setItem('user', JSON.stringify(userData));

      // 更新状态
      setToken(newToken);
      setUser(userData);
      setAuthState({
        isAuthenticated: true,
        isLoading: false,
      });

      return userData;
    } catch (error) {
      console.error('Login failed:', error);
      throw error;
    }
  };

  /**
   * 登出
   */
  const logout = useCallback(async () => {
    try {
      await authAPI.logout();
    } catch (error) {
      console.error('Logout failed:', error);
    } finally {
      clearAuth();
    }
  }, []);

  /**
   * 清除认证信息
   */
  const clearAuth = useCallback(() => {
    localStorage.removeItem('token');
    localStorage.removeItem('user');
    setToken(null);
    setUser(null);
    setAuthState({
      isAuthenticated: false,
      isLoading: false,
    });
  }, []);

  /**
   * 更新用户信息
   */
  const updateUser = useCallback((userData) => {
    setUser((prev) => ({ ...prev, ...userData }));
    localStorage.setItem('user', JSON.stringify({ ...user, ...userData }));
  }, [user]);

  /**
   * 检查权限
   */
  const hasRole = useCallback(
    (roles) => {
      if (!user) return false;
      if (Array.isArray(roles)) {
        return roles.includes(user.role);
      }
      return user.role === roles;
    },
    [user]
  );

  /**
   * 检查是否可以编辑
   */
  const canEdit = useCallback(
    (createdBy) => {
      if (!user) return false;
      if (user.role === 'admin') return true;
      if (user.role === 'market' && createdBy === user.id) return true;
      return false;
    },
    [user]
  );

  const value = {
    user,
    token,
    loading,
    authState,
    login,
    logout,
    checkAuth,
    updateUser,
    hasRole,
    canEdit,
    isAuthenticated: authState.isAuthenticated,
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

/**
 * 使用AuthContext的Hook
 */
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

export default AuthContext;

2.2 ThemeContext(主题上下文,可选)

// contexts/ThemeContext.jsx
import { createContext, useContext, useState, useCallback, useEffect } from 'react';

const ThemeContext = createContext(null);

export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  // 从localStorage加载主题
  useEffect(() => {
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      setTheme(savedTheme);
      document.documentElement.setAttribute('data-theme', savedTheme);
    }
  }, []);

  /**
   * 切换主题
   */
  const toggleTheme = useCallback(() => {
    setTheme((prev) => {
      const newTheme = prev === 'light' ? 'dark' : 'light';
      localStorage.setItem('theme', newTheme);
      document.documentElement.setAttribute('data-theme', newTheme);
      return newTheme;
    });
  }, []);

  /**
   * 设置主题
   */
  const setThemeMode = useCallback((mode) => {
    setTheme(mode);
    localStorage.setItem('theme', mode);
    document.documentElement.setAttribute('data-theme', mode);
  }, []);

  const value = {
    theme,
    toggleTheme,
    setThemeMode,
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
};

export default ThemeContext;

3. 自定义Hooks

3.1 useAuth Hook

// hooks/useAuth.js
import { useAuth as useAuthContext } from '../contexts/AuthContext';

export const useAuth = () => {
  return useAuthContext();
};

3.2 usePermission Hook

// hooks/usePermission.js
import { useAuth } from './useAuth';

/**
 * 权限检查Hook
 */
export const usePermission = () => {
  const { user, hasRole, canEdit } = useAuth();

  /**
   * 检查是否是管理员
   */
  const isAdmin = () => {
    return user?.role === 'admin';
  };

  /**
   * 检查是否是市场部
   */
  const isMarket = () => {
    return user?.role === 'market';
  };

  /**
   * 检查是否是其他部门
   */
  const isOther = () => {
    return user?.role === 'other';
  };

  /**
   * 检查是否有指定权限
   */
  const hasPermission = (permission) => {
    // 可以扩展更复杂的权限系统
    const permissions = {
      admin: ['create_user', 'edit_user', 'delete_user', 'create_project', 'edit_project', 'delete_project'],
      market: ['create_project', 'edit_own_project', 'delete_own_project'],
      other: ['edit_project_cost', 'edit_project_finance', 'edit_project_progress'],
    };

    return permissions[user?.role]?.includes(permission) || false;
  };

  return {
    isAdmin,
    isMarket,
    isOther,
    hasRole,
    canEdit,
    hasPermission,
    user,
  };
};

3.3 useModal Hook

// hooks/useModal.js
import { useState, useCallback } from 'react';

/**
 * 弹窗管理Hook
 */
export const useModal = () => {
  const [visible, setVisible] = useState(false);
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState(null);

  const open = useCallback((modalData) => {
    setData(modalData);
    setVisible(true);
  }, []);

  const close = useCallback(() => {
    setVisible(false);
    setData(null);
    setLoading(false);
  }, []);

  const toggle = useCallback(() => {
    setVisible((prev) => !prev);
  }, []);

  const setLoadingState = useCallback((state) => {
    setLoading(state);
  }, []);

  return {
    visible,
    loading,
    data,
    open,
    close,
    toggle,
    setLoading: setLoadingState,
  };
};

3.4 useTable Hook

// hooks/useTable.js
import { useState, useCallback } from 'react';

/**
 * 表格管理Hook
 */
export const useTable = (initialState = {}) => {
  const {
    pagination: initialPagination = { current: 1, pageSize: 10 },
    filters: initialFilters = {},
    sorter: initialSorter = {},
    selectedRowKeys: initialSelectedRowKeys = [],
  } = initialState;

  const [pagination, setPagination] = useState(initialPagination);
  const [filters, setFilters] = useState(initialFilters);
  const [sorter, setSorter] = useState(initialSorter);
  const [selectedRowKeys, setSelectedRowKeys] = useState(initialSelectedRowKeys);

  const handleTableChange = useCallback((newPagination, newFilters, newSorter) => {
    setPagination(newPagination);
    setFilters(newFilters);
    setSorter({
      field: newSorter.field,
      order: newSorter.order,
    });
  }, []);

  const resetTable = useCallback(() => {
    setPagination(initialPagination);
    setFilters(initialFilters);
    setSorter(initialSorter);
    setSelectedRowKeys(initialSelectedRowKeys);
  }, [initialPagination, initialFilters, initialSorter, initialSelectedRowKeys]);

  return {
    pagination,
    filters,
    sorter,
    selectedRowKeys,
    setSelectedRowKeys,
    handleTableChange,
    resetTable,
  };
};

3.5 useForm Hook

// hooks/useForm.js
import { useState, useCallback } from 'react';

/**
 * 表单管理Hook
 */
export const useForm = (initialValues = {}) => {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [submitting, setSubmitting] = useState(false);

  /**
   * 更新表单值
   */
  const handleChange = useCallback((field, value) => {
    setValues((prev) => ({
      ...prev,
      [field]: value,
    }));
    // 清除该字段的错误
    if (errors[field]) {
      setErrors((prev) => ({
        ...prev,
        [field]: null,
      }));
    }
  }, [errors]);

  /**
   * 批量更新表单值
   */
  const setFieldsValue = useCallback((newValues) => {
    setValues((prev) => ({
      ...prev,
      ...newValues,
    }));
  }, []);

  /**
   * 标记字段为已触摸
   */
  const handleBlur = useCallback((field) => {
    setTouched((prev) => ({
      ...prev,
      [field]: true,
    }));
  }, []);

  /**
   * 设置错误
   */
  const setFieldError = useCallback((field, error) => {
    setErrors((prev) => ({
      ...prev,
      [field]: error,
    }));
  }, []);

  /**
   * 批量设置错误
   */
  const setErrorsAll = useCallback((newErrors) => {
    setErrors(newErrors);
  }, []);

  /**
   * 验证表单
   */
  const validate = useCallback((validationRules) => {
    const newErrors = {};
    let isValid = true;

    Object.keys(validationRules).forEach((field) => {
      const rules = validationRules[field];
      const value = values[field];

      for (const rule of rules) {
        let error = null;

        if (rule.required && !value) {
          error = rule.message || `${field} is required`;
        } else if (rule.pattern && !rule.pattern.test(value)) {
          error = rule.message || `${field} is invalid`;
        } else if (rule.validator && !rule.validator(value)) {
          error = rule.message || `${field} is invalid`;
        }

        if (error) {
          newErrors[field] = error;
          isValid = false;
          break;
        }
      }
    });

    setErrors(newErrors);
    return isValid;
  }, [values]);

  /**
   * 重置表单
   */
  const resetForm = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
    setSubmitting(false);
  }, [initialValues]);

  /**
   * 提交表单
   */
  const handleSubmit = useCallback(async (onSubmit, validationRules) => {
    if (validationRules && !validate(validationRules)) {
      return false;
    }

    setSubmitting(true);
    try {
      await onSubmit(values);
      return true;
    } catch (error) {
      console.error('Form submit error:', error);
      return false;
    } finally {
      setSubmitting(false);
    }
  }, [values, validate]);

  return {
    values,
    errors,
    touched,
    submitting,
    handleChange,
    setFieldsValue,
    handleBlur,
    setFieldError,
    setErrorsAll,
    validate,
    resetForm,
    handleSubmit,
  };
};

3.6 useDebounce Hook

// hooks/useDebounce.js
import { useState, useEffect } from 'react';

/**
 * 防抖Hook
 */
export const useDebounce = (value, delay = 300) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
};

3.7 useLocalStorage Hook

// hooks/useLocalStorage.js
import { useState, useEffect, useCallback } from 'react';

/**
 * LocalStorage Hook
 */
export const useLocalStorage = (key, initialValue) => {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  const setValue = useCallback(
    (value) => {
      try {
        const valueToStore = value instanceof Function ? value(storedValue) : value;
        setStoredValue(valueToStore);
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      } catch (error) {
        console.error(`Error setting localStorage key "${key}":`, error);
      }
    },
    [key, storedValue]
  );

  const removeValue = useCallback(() => {
    try {
      window.localStorage.removeItem(key);
      setStoredValue(initialValue);
    } catch (error) {
      console.error(`Error removing localStorage key "${key}":`, error);
    }
  }, [key, initialValue]);

  return [storedValue, setValue, removeValue];
};

4. 状态管理最佳实践

4.1 状态分类

// 1. 本地状态 - 使用useState
const [value, setValue] = useState('');

// 2. 上下文状态 - 使用Context
const { user, login, logout } = useAuth();

// 3. URL状态 - 使用useSearchParams
const [searchParams, setSearchParams] = useSearchParams();

// 4. 表单状态 - 使用Form Hook
const [form] = Form.useForm();

// 5. 列表状态 - 使用自定义Hook
const { list, loading, pagination } = useList(projectAPI.getList);

4.2 状态提升

错误示例:

// 父组件
const Parent = () => {
  const [value, setValue] = useState('');
  return <Child value={value} onChange={setValue} />;
};

// 子组件
const Child = ({ value, onChange }) => {
  return <Input value={value} onChange={(e) => onChange(e.target.value)} />;
};

正确示例:

// 状态留在子组件
const Child = () => {
  const [value, setValue] = useState('');
  return <Input value={value} onChange={(e) => setValue(e.target.value)} />;
};

4.3 避免不必要的重渲染

// 使用React.memo
const ExpensiveComponent = React.memo(({ data }) => {
  return <div>{/* 渲染逻辑 */}</div>;
});

// 使用useMemo缓存计算结果
const expensiveValue = useMemo(() => {
  return computeExpensiveValue(data);
}, [data]);

// 使用useCallback缓存函数
const handleClick = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

5. 状态持久化

5.1 本地存储策略

// 需要持久化的状态
const persistentState = {
  user: 'localStorage',  // 用户信息
  token: 'localStorage',  // 认证令牌
  theme: 'localStorage', // 主题设置
  language: 'localStorage', // 语言设置
};

// 不需要持久化的状态
const transientState = {
  loading: false,       // 加载状态
  error: null,          // 错误信息
  modalVisible: false,  // 弹窗状态
  formData: {},         // 表单数据
};

5.2 状态恢复

// 在应用启动时恢复状态
useEffect(() => {
  const savedTheme = localStorage.getItem('theme');
  if (savedTheme) {
    setTheme(savedTheme);
  }

  const savedUser = localStorage.getItem('user');
  if (savedUser) {
    setUser(JSON.parse(savedUser));
  }
}, []);

6. 调试工具

6.1 React DevTools

// 安装React DevTools浏览器扩展
// 在开发环境中,可以使用React DevTools查看组件树和状态

6.2 自定义状态日志

// 开发环境下的状态日志
if (import.meta.env.DEV) {
  useEffect(() => {
    console.log('Auth state changed:', { user, token, authState });
  }, [user, token, authState]);
}

6.3 状态变化监听

// 监听特定状态的变化
useEffect(() => {
  if (user) {
    console.log('User logged in:', user);
  } else {
    console.log('User logged out');
  }
}, [user]);

7. 性能优化

7.1 减少Context更新

// 错误示例:频繁更新Context
const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [timestamp, setTimestamp] = useState(Date.now());

  // 这个timestamp变化会导致所有使用AuthContext的组件重渲染
  useEffect(() => {
    setInterval(() => {
      setTimestamp(Date.now());
    }, 1000);
  }, []);

  return <AuthContext.Provider value={{ user, timestamp }}>{children}</AuthContext.Provider>;
};

// 正确示例:分离频繁更新的状态
const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [timestamp, setTimestamp] = useState(Date.now());

  useEffect(() => {
    setInterval(() => {
      setTimestamp(Date.now());
    }, 1000);
  }, []);

  return (
    <>
      <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
      <TimestampContext.Provider value={{ timestamp}}>{/* 不渲染 */}</TimestampContext.Provider>
    </>
  );
};

7.2 使用useReducer

// 对于复杂的状态逻辑,使用useReducer
const initialState = {
  user: null,
  token: null,
  loading: true,
  error: null,
};

function authReducer(state, action) {
  switch (action.type) {
    case 'LOGIN_SUCCESS':
      return {
        ...state,
        user: action.payload.user,
        token: action.payload.token,
        loading: false,
      };
    case 'LOGOUT':
      return {
        ...state,
        user: null,
        token: null,
      };
    default:
      return state;
  }
}

const AuthProvider = ({ children }) => {
  const [state, dispatch] = useReducer(authReducer, initialState);

  return (
    <AuthContext.Provider value={{ state, dispatch }}>
      {children}
    </AuthContext.Provider>
  );
};

8. 未来扩展

8.1 迁移到Redux

如果项目变得复杂,可以考虑迁移到Redux Toolkit

// slices/authSlice.js
import { createSlice } from '@reduxjs/toolkit';

const authSlice = createSlice({
  name: 'auth',
  initialState: {
    user: null,
    token: null,
    loading: true,
  },
  reducers: {
    setCredentials: (state, action) => {
      state.user = action.payload.user;
      state.token = action.payload.token;
      state.loading = false;
    },
    logout: (state) => {
      state.user = null;
      state.token = null;
    },
  },
});

export const { setCredentials, logout } = authSlice.actions;
export default authSlice.reducer;

8.2 迁移到Zustand

Zustand是一个更轻量级的替代方案:

// store/authStore.js
import { create } from 'zustand';

export const useAuthStore = create((set) => ({
  user: null,
  token: null,
  loading: true,
  login: (user, token) => set({ user, token, loading: false }),
  logout: () => set({ user: null, token: null }),
}));

文档维护: 前端程序员 文档类型: 状态管理设计文档 最后更新: 2026-01-25