Skip to content
刘恩义的技术博客
返回

React 状态管理最佳实践

Edit page

状态管理是 React 应用开发中的核心挑战之一。随着应用规模的增长,状态管理变得越来越复杂,选择合适的状态管理方案对于构建可维护、高性能的应用至关重要。本文将详细介绍 React 状态管理的各种方案,包括 Context API、Redux、Zustand 等,并提供实战案例与性能优化策略。

1. 状态管理基础

1.1 状态类型

React 应用中的状态可以分为以下几种类型:

1.2 状态管理原则

1.3 状态管理方案选择

方案适用场景优点缺点
useState局部状态管理简单易用,内置 Hook无法跨组件共享
useReducer复杂局部状态更强大的状态逻辑管理代码量增加
Context API小型应用的共享状态内置 API,无需额外依赖可能导致不必要的重渲染
Redux大型应用的全局状态强大的中间件生态,可预测的状态管理配置复杂,代码冗余
Zustand中型应用的状态管理简单易用,性能优秀生态相对较小
Jotai细粒度状态管理原子化状态,灵活组合学习曲线较陡
RecoilFacebook 开发的状态管理库强大的状态派生和缓存仍在开发中

2. 内置状态管理方案

2.1 useState

useState 是 React 中最基本的状态管理 Hook,适用于简单的局部状态:

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('Hello');
  
  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Message: {message}</p>
      <button onClick={increment}>Increment</button>
      <input 
        type="text" 
        value={message} 
        onChange={(e) => setMessage(e.target.value)} 
      />
    </div>
  );
}

2.2 useReducer

useReducer 适用于更复杂的状态管理,尤其是当状态更新逻辑依赖于之前的状态或包含多个子值时:

import { useReducer } from 'react';

interface State {
  count: number;
  step: number;
}

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'setStep'; payload: number }
  | { type: 'reset' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      return { ...state, step: action.payload };
    case 'reset':
      return { ...state, count: 0 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <p>Step: {state.step}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <input 
        type="number" 
        value={state.step} 
        onChange={(e) => dispatch({ type: 'setStep', payload: Number(e.target.value) })} 
      />
    </div>
  );
}

2.3 Context API

Context API 适用于在组件树中共享状态,无需通过 props 层层传递:

import { createContext, useContext, useState, ReactNode } from 'react';

interface ThemeContextType {
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

interface ThemeProviderProps {
  children: ReactNode;
}

function ThemeProvider({ children }: ThemeProviderProps) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

function ThemedButton() {
  const { theme } = useTheme();
  
  return (
    <button 
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
        padding: '8px 16px',
        border: '1px solid #ccc'
      }}
    >
      Themed Button
    </button>
  );
}

function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Toggle {theme === 'light' ? 'Dark' : 'Light'} Mode
    </button>
  );
}

function App() {
  return (
    <ThemeProvider>
      <div>
        <h1>Context API Example</h1>
        <ThemedButton />
        <ThemeToggle />
      </div>
    </ThemeProvider>
  );
}

3. 第三方状态管理库

3.1 Redux Toolkit

Redux Toolkit 是官方推荐的 Redux 开发方式,简化了 Redux 的配置和使用:

// src/store.ts
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
  status: 'idle' | 'loading' | 'succeeded' | 'failed';
}

const initialState: CounterState = {
  value: 0,
  status: 'idle',
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchCount.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchCount.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.value = action.payload;
      })
      .addCase(fetchCount.rejected, (state) => {
        state.status = 'failed';
      });
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export const fetchCount = createAsyncThunk(
  'counter/fetchCount',
  async (amount: number) => {
    const response = await fetch(`https://api.example.com/count?amount=${amount}`);
    const data = await response.json();
    return data.count;
  }
);

export const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// src/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// src/App.tsx
import { useAppSelector, useAppDispatch } from './hooks';
import { increment, decrement, incrementByAmount, fetchCount } from './store';

function Counter() {
  const count = useAppSelector((state) => state.counter.value);
  const status = useAppSelector((state) => state.counter.status);
  const dispatch = useAppDispatch();
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>Increment by 5</button>
      <button 
        onClick={() => dispatch(fetchCount(10))} 
        disabled={status === 'loading'}
      >
        {status === 'loading' ? 'Loading...' : 'Fetch Count'}
      </button>
    </div>
  );
}

function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

3.2 Zustand

Zustand 是一个轻量级的状态管理库,使用简单,性能优秀:

// src/store.ts
import create from 'zustand';

interface CounterState {
  count: number;
  increment: () => void;
  decrement: () => void;
  incrementByAmount: (amount: number) => void;
  reset: () => void;
}

const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  incrementByAmount: (amount) => set((state) => ({ count: state.count + amount })),
  reset: () => set({ count: 0 }),
}));

// src/App.tsx
import { useCounterStore } from './store';

function Counter() {
  const { count, increment, decrement, incrementByAmount, reset } = useCounterStore();
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={() => incrementByAmount(5)}>Increment by 5</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

function App() {
  return <Counter />;
}

3.3 Jotai

Jotai 是一个原子化的状态管理库,适用于细粒度的状态管理:

// src/atoms.ts
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';

// 原子状态
const countAtom = atom(0);
const messageAtom = atom('Hello');

// 派生状态
const doubledCountAtom = atom((get) => get(countAtom) * 2);

// 异步状态
const asyncCountAtom = atom(
  async () => {
    const response = await fetch('https://api.example.com/count');
    const data = await response.json();
    return data.count;
  }
);

// src/App.tsx
import { countAtom, doubledCountAtom, messageAtom, asyncCountAtom } from './atoms';

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const doubledCount = useAtomValue(doubledCountAtom);
  const [message, setMessage] = useAtom(messageAtom);
  const [asyncCount, setAsyncCount] = useAtom(asyncCountAtom);
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled Count: {doubledCount}</p>
      <p>Message: {message}</p>
      <p>Async Count: {asyncCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <input 
        type="text" 
        value={message} 
        onChange={(e) => setMessage(e.target.value)} 
      />
      <button onClick={() => setAsyncCount(Math.random() * 100)}>Update Async Count</button>
    </div>
  );
}

function App() {
  return <Counter />;
}

4. 状态管理最佳实践

4.1 状态组织

4.2 性能优化

4.3 类型安全

4.4 测试

4.5 调试

5. 实战案例

5.1 认证状态管理

功能需求

实现方案:使用 Zustand 管理认证状态

// src/store/authStore.ts
import create from 'zustand';
import { persist } from 'zustand/middleware';

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface AuthState {
  user: User | null;
  isLoading: boolean;
  error: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  clearError: () => void;
}

const useAuthStore = create<AuthState>(
  persist(
    (set, get) => ({
      user: null,
      isLoading: false,
      error: null,
      login: async (email, password) => {
        set({ isLoading: true, error: null });
        try {
          const response = await fetch('/api/login', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({ email, password }),
          });
          if (!response.ok) {
            throw new Error('Login failed');
          }
          const user = await response.json();
          set({ user, isLoading: false });
        } catch (error) {
          set({ error: error instanceof Error ? error.message : 'Login failed', isLoading: false });
        }
      },
      logout: () => {
        set({ user: null });
        // 可选:调用登出 API
        fetch('/api/logout', {
          method: 'POST',
        });
      },
      clearError: () => {
        set({ error: null });
      },
    }),
    {
      name: 'auth-storage', // 本地存储键名
    }
  )
);

export default useAuthStore;

// src/components/AuthGuard.tsx
import { Navigate } from 'react-router-dom';
import useAuthStore from '../store/authStore';

interface AuthGuardProps {
  children: React.ReactNode;
  requiredRole?: 'admin' | 'user';
}

function AuthGuard({ children, requiredRole = 'user' }: AuthGuardProps) {
  const { user, isLoading } = useAuthStore();
  
  if (isLoading) {
    return <div>Loading...</div>;
  }
  
  if (!user) {
    return <Navigate to="/login" replace />;
  }
  
  if (requiredRole === 'admin' && user.role !== 'admin') {
    return <Navigate to="/unauthorized" replace />;
  }
  
  return <>{children}</>;
}

export default AuthGuard;

// src/pages/Login.tsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import useAuthStore from '../store/authStore';

function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const navigate = useNavigate();
  const { login, isLoading, error, clearError } = useAuthStore();
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    clearError();
    await login(email, password);
    navigate('/dashboard');
  };
  
  return (
    <div>
      <h1>Login</h1>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email:</label>
          <input
            type="email"
            id="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
          />
        </div>
        <div>
          <label htmlFor="password">Password:</label>
          <input
            type="password"
            id="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
          />
        </div>
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Login'}
        </button>
      </form>
    </div>
  );
}

export default Login;

5.2 购物车状态管理

功能需求

实现方案:使用 Redux Toolkit 管理购物车状态

// src/store/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface Product {
  id: string;
  name: string;
  price: number;
  image: string;
}

interface CartItem extends Product {
  quantity: number;
}

interface CartState {
  items: CartItem[];
  total: number;
}

const initialState: CartState = {
  items: [],
  total: 0,
};

const cartSlice = createSlice({
  name: 'cart',
  initialState,
  reducers: {
    addToCart: (state, action: PayloadAction<Product>) => {
      const existingItem = state.items.find(item => item.id === action.payload.id);
      if (existingItem) {
        existingItem.quantity += 1;
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }
      state.total = state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    },
    removeFromCart: (state, action: PayloadAction<string>) => {
      state.items = state.items.filter(item => item.id !== action.payload);
      state.total = state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    },
    updateQuantity: (state, action: PayloadAction<{ id: string; quantity: number }>) => {
      const { id, quantity } = action.payload;
      const item = state.items.find(item => item.id === id);
      if (item) {
        item.quantity = Math.max(1, quantity);
        state.total = state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
      }
    },
    clearCart: (state) => {
      state.items = [];
      state.total = 0;
    },
  },
});

export const { addToCart, removeFromCart, updateQuantity, clearCart } = cartSlice.actions;

export default cartSlice.reducer;

// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';

const store = configureStore({
  reducer: {
    cart: cartReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

// src/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// src/components/CartItem.tsx
import { useAppDispatch } from '../hooks';
import { removeFromCart, updateQuantity } from '../store/cartSlice';

interface CartItemProps {
  item: {
    id: string;
    name: string;
    price: number;
    quantity: number;
    image: string;
  };
}

function CartItem({ item }: CartItemProps) {
  const dispatch = useAppDispatch();
  
  const handleRemove = () => {
    dispatch(removeFromCart(item.id));
  };
  
  const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const quantity = Number(e.target.value);
    dispatch(updateQuantity({ id: item.id, quantity }));
  };
  
  return (
    <div className="cart-item">
      <img src={item.image} alt={item.name} className="item-image" />
      <div className="item-details">
        <h3>{item.name}</h3>
        <p>${item.price.toFixed(2)}</p>
        <div className="quantity-control">
          <button 
            onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity - 1 }))}
            disabled={item.quantity === 1}
          >
            -
          </button>
          <input 
            type="number" 
            value={item.quantity} 
            onChange={handleQuantityChange}
            min="1"
          />
          <button 
            onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity + 1 }))}
          >
            +
          </button>
        </div>
        <button onClick={handleRemove} className="remove-button">
          Remove
        </button>
      </div>
    </div>
  );
}

export default CartItem;

// src/pages/Cart.tsx
import { useAppSelector, useAppDispatch } from '../hooks';
import { clearCart } from '../store/cartSlice';
import CartItem from '../components/CartItem';
import { useNavigate } from 'react-router-dom';

function Cart() {
  const { items, total } = useAppSelector(state => state.cart);
  const dispatch = useAppDispatch();
  const navigate = useNavigate();
  
  const handleCheckout = () => {
    navigate('/checkout');
  };
  
  const handleClearCart = () => {
    dispatch(clearCart());
  };
  
  if (items.length === 0) {
    return (
      <div className="cart-empty">
        <h1>Your Cart</h1>
        <p>Your cart is empty</p>
        <button onClick={() => navigate('/products')}>
          Continue Shopping
        </button>
      </div>
    );
  }
  
  return (
    <div className="cart">
      <h1>Your Cart</h1>
      <div className="cart-items">
        {items.map(item => (
          <CartItem key={item.id} item={item} />
        ))}
      </div>
      <div className="cart-summary">
        <h2>Summary</h2>
        <p>Total Items: {items.reduce((sum, item) => sum + item.quantity, 0)}</p>
        <p>Total Price: ${total.toFixed(2)}</p>
        <button onClick={handleClearCart} className="clear-button">
          Clear Cart
        </button>
        <button onClick={handleCheckout} className="checkout-button">
          Proceed to Checkout
        </button>
      </div>
    </div>
  );
}

export default Cart;

5.3 主题状态管理

功能需求

实现方案:使用 Context API 管理主题状态

// src/context/ThemeContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';

type Theme = 'light' | 'dark';

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
  isLoading: boolean;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

interface ThemeProviderProps {
  children: ReactNode;
}

function ThemeProvider({ children }: ThemeProviderProps) {
  const [theme, setTheme] = useState<Theme>('light');
  const [isLoading, setIsLoading] = useState(true);
  
  // 从本地存储加载主题
  useEffect(() => {
    const savedTheme = localStorage.getItem('theme') as Theme | null;
    if (savedTheme) {
      setTheme(savedTheme);
    } else {
      // 检测系统主题
      const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      setTheme(prefersDark ? 'dark' : 'light');
    }
    setIsLoading(false);
  }, []);
  
  // 保存主题到本地存储
  useEffect(() => {
    localStorage.setItem('theme', theme);
    // 更新文档根元素的类名
    document.documentElement.className = theme;
  }, [theme]);
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  if (isLoading) {
    return <div>Loading...</div>;
  }
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme, isLoading: false }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

export { ThemeProvider, useTheme };

// src/styles/theme.css
:root,
.light {
  --background: #ffffff;
  --foreground: #000000;
  --primary: #007bff;
  --secondary: #6c757d;
  --border: #dee2e6;
  --card: #f8f9fa;
}

.dark {
  --background: #121212;
  --foreground: #ffffff;
  --primary: #00bfff;
  --secondary: #adb5bd;
  --border: #343a40;
  --card: #1e1e1e;
}

body {
  background-color: var(--background);
  color: var(--foreground);
  transition: background-color 0.3s, color 0.3s;
}

.card {
  background-color: var(--card);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 16px;
  margin: 8px 0;
}

button {
  background-color: var(--primary);
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  opacity: 0.8;
}

button.secondary {
  background-color: var(--secondary);
}

// src/components/ThemeToggle.tsx
import { useTheme } from '../context/ThemeContext';

function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <button onClick={toggleTheme} className="secondary">
      Toggle {theme === 'light' ? 'Dark' : 'Light'} Mode
    </button>
  );
}

export default ThemeToggle;

// src/App.tsx
import { ThemeProvider } from './context/ThemeContext';
import ThemeToggle from './components/ThemeToggle';
import './styles/theme.css';

function App() {
  return (
    <ThemeProvider>
      <div>
        <h1>Theme Context Example</h1>
        <div className="card">
          <p>This is a card component</p>
        </div>
        <ThemeToggle />
      </div>
    </ThemeProvider>
  );
}

export default App;

6. 总结

React 状态管理是构建高质量 React 应用的关键部分,本文介绍了多种状态管理方案,包括:

  1. 内置方案useStateuseReducer 和 Context API,适用于不同复杂度的状态管理需求
  2. 第三方库:Redux Toolkit、Zustand 和 Jotai,提供了更强大、更灵活的状态管理能力
  3. 最佳实践:状态组织、性能优化、类型安全、测试和调试
  4. 实战案例:认证状态、购物车状态和主题状态的实现

选择合适的状态管理方案需要考虑应用的规模、复杂度和团队的熟悉程度。对于小型应用,内置的 Context API 可能已经足够;对于中型应用,Zustand 或 Jotai 可能是更好的选择;对于大型应用,Redux Toolkit 提供了更全面的解决方案。

无论选择哪种方案,都应该遵循状态管理的基本原则:单一数据源、状态不可变性、纯函数更新和明确的状态流转。通过合理的状态管理,你可以构建出更可维护、更高性能的 React 应用。


Edit page

Previous Post
React 表单处理最佳实践
Next Post
React Router 6 最佳实践