Skip to content

React 状态管理 Redux

概述

Redux 是一个可预测的状态容器,用于管理应用的全局状态。本章将学习 Redux 的核心概念、与 React 的集成,以及现代 Redux Toolkit 的使用。

🏪 Redux 核心概念

基础 Redux 设置

jsx
// 模拟 Redux 功能
function createStore(reducer, initialState) {
  let state = initialState;
  let listeners = [];
  
  const getState = () => state;
  
  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  };
  
  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  };
  
  return { getState, dispatch, subscribe };
}

// Action Types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
const SET_COUNT = 'SET_COUNT';

// Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const reset = () => ({ type: RESET });
const setCount = (count) => ({ type: SET_COUNT, payload: count });

// Reducer
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case INCREMENT:
      return { ...state, count: state.count + 1 };
    case DECREMENT:
      return { ...state, count: state.count - 1 };
    case RESET:
      return { ...state, count: 0 };
    case SET_COUNT:
      return { ...state, count: action.payload };
    default:
      return state;
  }
}

// React Redux 连接
const ReduxContext = React.createContext();

function Provider({ store, children }) {
  const [, forceUpdate] = React.useReducer(x => x + 1, 0);
  
  React.useEffect(() => {
    const unsubscribe = store.subscribe(forceUpdate);
    return unsubscribe;
  }, [store]);
  
  return (
    <ReduxContext.Provider value={store}>
      {children}
    </ReduxContext.Provider>
  );
}

function useSelector(selector) {
  const store = React.useContext(ReduxContext);
  return selector(store.getState());
}

function useDispatch() {
  const store = React.useContext(ReduxContext);
  return store.dispatch;
}

// 计数器组件
function Counter() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();
  
  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h2>Redux 计数器</h2>
      <div style={{ fontSize: '48px', margin: '20px 0', color: '#007bff' }}>
        {count}
      </div>
      <div>
        <button 
          onClick={() => dispatch(decrement())}
          style={{ margin: '0 10px', padding: '10px 20px' }}
        >
          -1
        </button>
        <button 
          onClick={() => dispatch(increment())}
          style={{ margin: '0 10px', padding: '10px 20px' }}
        >
          +1
        </button>
        <button 
          onClick={() => dispatch(reset())}
          style={{ margin: '0 10px', padding: '10px 20px' }}
        >
          重置
        </button>
      </div>
    </div>
  );
}

function BasicReduxExample() {
  const store = React.useMemo(() => createStore(counterReducer), []);
  
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

复杂状态管理

jsx
// 待办事项 Actions
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
const SET_FILTER = 'SET_FILTER';

const addTodo = (text) => ({
  type: ADD_TODO,
  payload: { id: Date.now(), text, completed: false }
});

const toggleTodo = (id) => ({
  type: TOGGLE_TODO,
  payload: id
});

const deleteTodo = (id) => ({
  type: DELETE_TODO,
  payload: id
});

const setFilter = (filter) => ({
  type: SET_FILTER,
  payload: filter
});

// 待办事项 Reducer
function todosReducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [...state, action.payload];
    case TOGGLE_TODO:
      return state.map(todo =>
        todo.id === action.payload
          ? { ...todo, completed: !todo.completed }
          : todo
      );
    case DELETE_TODO:
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

// 过滤器 Reducer
function filterReducer(state = 'ALL', action) {
  switch (action.type) {
    case SET_FILTER:
      return action.payload;
    default:
      return state;
  }
}

// 根 Reducer
function rootReducer(state = {}, action) {
  return {
    todos: todosReducer(state.todos, action),
    filter: filterReducer(state.filter, action)
  };
}

// 待办事项组件
function TodoApp() {
  const todos = useSelector(state => state.todos || []);
  const filter = useSelector(state => state.filter || 'ALL');
  const dispatch = useDispatch();
  
  const [inputValue, setInputValue] = React.useState('');
  
  const filteredTodos = todos.filter(todo => {
    switch (filter) {
      case 'ACTIVE':
        return !todo.completed;
      case 'COMPLETED':
        return todo.completed;
      default:
        return true;
    }
  });
  
  const handleAddTodo = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      dispatch(addTodo(inputValue.trim()));
      setInputValue('');
    }
  };
  
  const todoStats = {
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length
  };
  
  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
      <h2>Redux 待办事项</h2>
      
      {/* 添加待办事项 */}
      <form onSubmit={handleAddTodo} style={{ marginBottom: '20px' }}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="添加新的待办事项..."
          style={{ 
            padding: '10px', 
            width: '70%', 
            border: '1px solid #ddd',
            borderRadius: '4px 0 0 4px'
          }}
        />
        <button 
          type="submit"
          style={{ 
            padding: '10px 20px', 
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '0 4px 4px 0'
          }}
        >
          添加
        </button>
      </form>
      
      {/* 过滤器 */}
      <div style={{ marginBottom: '20px' }}>
        {['ALL', 'ACTIVE', 'COMPLETED'].map(filterType => (
          <button
            key={filterType}
            onClick={() => dispatch(setFilter(filterType))}
            style={{
              margin: '0 5px',
              padding: '8px 16px',
              backgroundColor: filter === filterType ? '#007bff' : '#f8f9fa',
              color: filter === filterType ? 'white' : '#333',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}
          >
            {filterType === 'ALL' ? '全部' : filterType === 'ACTIVE' ? '进行中' : '已完成'}
          </button>
        ))}
      </div>
      
      {/* 统计信息 */}
      <div style={{ 
        backgroundColor: '#f8f9fa', 
        padding: '15px', 
        borderRadius: '8px',
        marginBottom: '20px'
      }}>
        <div>总计: {todoStats.total} | 进行中: {todoStats.active} | 已完成: {todoStats.completed}</div>
      </div>
      
      {/* 待办事项列表 */}
      <div>
        {filteredTodos.length === 0 ? (
          <div style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
            {filter === 'ALL' ? '暂无待办事项' : '没有符合条件的待办事项'}
          </div>
        ) : (
          filteredTodos.map(todo => (
            <div
              key={todo.id}
              style={{
                display: 'flex',
                alignItems: 'center',
                padding: '12px',
                margin: '8px 0',
                backgroundColor: 'white',
                border: '1px solid #ddd',
                borderRadius: '8px',
                boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
              }}
            >
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => dispatch(toggleTodo(todo.id))}
                style={{ marginRight: '12px' }}
              />
              <span
                style={{
                  flex: 1,
                  textDecoration: todo.completed ? 'line-through' : 'none',
                  color: todo.completed ? '#6c757d' : '#333'
                }}
              >
                {todo.text}
              </span>
              <button
                onClick={() => dispatch(deleteTodo(todo.id))}
                style={{
                  backgroundColor: '#dc3545',
                  color: 'white',
                  border: 'none',
                  padding: '6px 12px',
                  borderRadius: '4px',
                  cursor: 'pointer'
                }}
              >
                删除
              </button>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

function ComplexReduxExample() {
  const store = React.useMemo(() => createStore(rootReducer), []);
  
  return (
    <Provider store={store}>
      <TodoApp />
    </Provider>
  );
}

🛠️ Redux Toolkit 现代方法

Slice 和现代 Redux

jsx
// 模拟 Redux Toolkit createSlice
function createSlice({ name, initialState, reducers }) {
  const actionTypes = {};
  const actionCreators = {};
  
  Object.keys(reducers).forEach(key => {
    const type = `${name}/${key}`;
    actionTypes[key] = type;
    actionCreators[key] = (payload) => ({ type, payload });
  });
  
  const reducer = (state = initialState, action) => {
    const reducerKey = Object.keys(actionTypes).find(
      key => actionTypes[key] === action.type
    );
    
    if (reducerKey && reducers[reducerKey]) {
      return reducers[reducerKey](state, action);
    }
    
    return state;
  };
  
  return {
    name,
    reducer,
    actions: actionCreators
  };
}

// 用户 Slice
const userSlice = createSlice({
  name: 'user',
  initialState: {
    profile: null,
    loading: false,
    error: null
  },
  reducers: {
    setLoading: (state, action) => ({
      ...state,
      loading: action.payload
    }),
    setProfile: (state, action) => ({
      ...state,
      profile: action.payload,
      loading: false,
      error: null
    }),
    setError: (state, action) => ({
      ...state,
      error: action.payload,
      loading: false
    }),
    clearUser: (state) => ({
      ...state,
      profile: null,
      error: null
    })
  }
});

// 购物车 Slice
const cartSlice = createSlice({
  name: 'cart',
  initialState: {
    items: [],
    total: 0
  },
  reducers: {
    addItem: (state, action) => {
      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);
      return state;
    },
    removeItem: (state, action) => {
      state.items = state.items.filter(item => item.id !== action.payload);
      state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
      return state;
    },
    updateQuantity: (state, action) => {
      const item = state.items.find(item => item.id === action.payload.id);
      if (item) {
        item.quantity = action.payload.quantity;
        if (item.quantity <= 0) {
          state.items = state.items.filter(i => i.id !== item.id);
        }
      }
      state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
      return state;
    },
    clearCart: (state) => {
      state.items = [];
      state.total = 0;
      return state;
    }
  }
});

// 组合 Reducer
function combineReducers(slices) {
  return (state = {}, action) => {
    const newState = {};
    Object.keys(slices).forEach(key => {
      newState[key] = slices[key].reducer(state[key], action);
    });
    return newState;
  };
}

const rootReducerToolkit = combineReducers({
  user: userSlice,
  cart: cartSlice
});

// 电商应用组件
function ECommerceApp() {
  const user = useSelector(state => state.user);
  const cart = useSelector(state => state.cart);
  const dispatch = useDispatch();
  
  const products = [
    { id: 1, name: 'iPhone 15', price: 5999 },
    { id: 2, name: 'MacBook Pro', price: 12999 },
    { id: 3, name: 'AirPods Pro', price: 1899 }
  ];
  
  const handleLogin = () => {
    dispatch(userSlice.actions.setLoading(true));
    setTimeout(() => {
      dispatch(userSlice.actions.setProfile({
        id: 1,
        name: '张三',
        email: 'zhangsan@example.com'
      }));
    }, 1000);
  };
  
  const handleLogout = () => {
    dispatch(userSlice.actions.clearUser());
    dispatch(cartSlice.actions.clearCart());
  };
  
  return (
    <div style={{ padding: '20px' }}>
      <header style={{ 
        display: 'flex', 
        justifyContent: 'space-between', 
        alignItems: 'center',
        marginBottom: '30px',
        paddingBottom: '20px',
        borderBottom: '1px solid #ddd'
      }}>
        <h1>Redux Toolkit 电商演示</h1>
        <div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
          {user.profile ? (
            <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
              <span>欢迎, {user.profile.name}</span>
              <button onClick={handleLogout}>登出</button>
            </div>
          ) : (
            <button onClick={handleLogin} disabled={user.loading}>
              {user.loading ? '登录中...' : '登录'}
            </button>
          )}
          <div style={{ 
            backgroundColor: '#007bff', 
            color: 'white', 
            padding: '8px 12px', 
            borderRadius: '20px' 
          }}>
            购物车: {cart.items.length} 件商品 ¥{cart.total}
          </div>
        </div>
      </header>
      
      <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '30px' }}>
        {/* 商品列表 */}
        <div>
          <h2>商品列表</h2>
          <div style={{ display: 'grid', gap: '15px' }}>
            {products.map(product => (
              <div key={product.id} style={{
                padding: '20px',
                border: '1px solid #ddd',
                borderRadius: '8px',
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center'
              }}>
                <div>
                  <h3 style={{ margin: '0 0 5px 0' }}>{product.name}</h3>
                  <p style={{ margin: 0, color: '#007bff', fontSize: '18px', fontWeight: 'bold' }}>
                    ¥{product.price}
                  </p>
                </div>
                <button
                  onClick={() => dispatch(cartSlice.actions.addItem(product))}
                  style={{
                    backgroundColor: '#28a745',
                    color: 'white',
                    border: 'none',
                    padding: '10px 20px',
                    borderRadius: '6px'
                  }}
                >
                  加入购物车
                </button>
              </div>
            ))}
          </div>
        </div>
        
        {/* 购物车 */}
        <div>
          <h2>购物车</h2>
          {cart.items.length === 0 ? (
            <div style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
              购物车为空
            </div>
          ) : (
            <div>
              {cart.items.map(item => (
                <div key={item.id} style={{
                  padding: '15px',
                  border: '1px solid #ddd',
                  borderRadius: '8px',
                  marginBottom: '10px'
                }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                    <div>
                      <h4 style={{ margin: '0 0 5px 0' }}>{item.name}</h4>
                      <p style={{ margin: 0, color: '#666' }}>¥{item.price} x {item.quantity}</p>
                    </div>
                    <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
                      <button
                        onClick={() => dispatch(cartSlice.actions.updateQuantity({
                          id: item.id,
                          quantity: item.quantity - 1
                        }))}
                        style={{ padding: '5px 10px' }}
                      >
                        -
                      </button>
                      <span>{item.quantity}</span>
                      <button
                        onClick={() => dispatch(cartSlice.actions.updateQuantity({
                          id: item.id,
                          quantity: item.quantity + 1
                        }))}
                        style={{ padding: '5px 10px' }}
                      >
                        +
                      </button>
                      <button
                        onClick={() => dispatch(cartSlice.actions.removeItem(item.id))}
                        style={{ color: 'red', background: 'none', border: 'none' }}
                      >
                        删除
                      </button>
                    </div>
                  </div>
                </div>
              ))}
              <div style={{ 
                padding: '15px', 
                backgroundColor: '#f8f9fa', 
                borderRadius: '8px',
                textAlign: 'center',
                fontSize: '18px',
                fontWeight: 'bold'
              }}>
                总计: ¥{cart.total}
              </div>
              <button
                onClick={() => dispatch(cartSlice.actions.clearCart())}
                style={{
                  width: '100%',
                  padding: '15px',
                  backgroundColor: '#dc3545',
                  color: 'white',
                  border: 'none',
                  borderRadius: '8px',
                  marginTop: '10px',
                  fontSize: '16px'
                }}
              >
                清空购物车
              </button>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function ReduxToolkitExample() {
  const store = React.useMemo(() => createStore(rootReducerToolkit), []);
  
  return (
    <Provider store={store}>
      <ECommerceApp />
    </Provider>
  );
}

📝 本章小结

通过本章学习,你应该掌握了:

Redux 核心概念

  • ✅ Store、Action、Reducer 的作用
  • ✅ 单向数据流和状态不可变性
  • ✅ React-Redux 的连接方式
  • ✅ Redux Toolkit 的现代用法

最佳实践

  1. 状态结构设计:保持状态扁平化和规范化
  2. Action 设计:使用描述性的 Action 类型
  3. Reducer 纯函数:避免副作用和状态突变
  4. 性能优化:使用 selector 优化和 memo
  5. 开发工具:利用 Redux DevTools 调试

继续学习下一章 - React 项目构建与部署

本站内容仅供学习和研究使用。