Skip to content

Redux

本文介绍Redux状态管理库,包括核心概念(Store、Action、Reducer)、工作流程以及在React应用中的集成方法。

什么是Redux

Redux是JavaScript应用的可预测状态容器,它提供了一种可靠的方式来管理应用的状态,特别适合于中大型单页应用(SPA)。Redux遵循三个基本原则:

  1. 单一数据源:整个应用的状态存储在单个store的对象树中
  2. 状态只读:唯一改变状态的方法是触发action
  3. 使用纯函数修改:通过reducer(纯函数)来指定状态树的变化

核心概念

Store

Store是保存应用状态的对象,它提供了以下API:

  • getState() - 获取当前状态
  • dispatch(action) - 触发状态更新
  • subscribe(listener) - 注册监听器
  • replaceReducer(nextReducer) - 替换reducer(高级用法)

一个应用只有一个store,它包含了应用的所有状态。

javascript
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);

Action

Action是描述发生了什么的普通JavaScript对象,它必须有一个type属性来表示动作的类型。

javascript
// 简单的action对象
{
  type: 'ADD_TODO',
  text: '学习Redux'
}

// 使用action creator函数创建action
function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  };
}

Reducer

Reducer是纯函数,接收当前状态和一个action,返回新的状态。

javascript
function todoReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: Date.now(),
          text: action.text,
          completed: false
        }
      ];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id
          ? { ...todo, completed: !todo.completed }
          : todo
      );
    default:
      return state;
  }
}

组合Reducers

对于复杂应用,可以将reducer拆分为多个小的reducer,每个负责状态树的一部分,然后使用combineReducers组合它们。

javascript
import { combineReducers } from 'redux';
import todos from './todos';
import visibilityFilter from './visibilityFilter';

const rootReducer = combineReducers({
  todos,
  visibilityFilter
});

export default rootReducer;

Redux工作流程

  1. 用户与UI交互,触发事件
  2. 事件处理函数dispatch一个action
  3. Redux store调用reducer函数
  4. Reducer计算新状态并返回
  5. Store更新状态,并通知所有订阅者
  6. 订阅了store的UI组件获取新状态并重新渲染
┌─────────────────────────────────────────┐
│                                         │
│  ┌─────────┐       ┌──────────────┐    │
│  │ Actions │──────▶│   Reducers   │    │
│  └─────────┘       └──────────────┘    │
│       ▲                    │           │
│       │                    ▼           │
│  ┌─────────┐       ┌──────────────┐    │
│  │   UI    │◀──────│    Store     │    │
│  └─────────┘       └──────────────┘    │
│                                         │
└─────────────────────────────────────────┘

在React中使用Redux

安装依赖

bash
npm install redux react-redux
# 或
yarn add redux react-redux

创建Store

javascript
// src/store/index.js
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(
  rootReducer,
  // 可选:使用Redux DevTools扩展
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

export default store;

提供Store

使用Provider组件将store提供给整个应用:

jsx
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

连接组件

使用connect高阶组件或useSelector/useDispatch Hooks连接组件与Redux:

使用connect(类组件)

jsx
import React from 'react';
import { connect } from 'react-redux';
import { addTodo, toggleTodo } from '../actions';

class TodoList extends React.Component {
  render() {
    const { todos, addTodo, toggleTodo } = this.props;
    
    return (
      <div>
        <button onClick={() => addTodo('新任务')}>添加任务</button>
        <ul>
          {todos.map(todo => (
            <li
              key={todo.id}
              onClick={() => toggleTodo(todo.id)}
              style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
            >
              {todo.text}
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

const mapStateToProps = state => ({
  todos: state.todos
});

const mapDispatchToProps = {
  addTodo,
  toggleTodo
};

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

使用Hooks(函数组件)

jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo } from '../actions';

function TodoList() {
  const todos = useSelector(state => state.todos);
  const dispatch = useDispatch();
  
  return (
    <div>
      <button onClick={() => dispatch(addTodo('新任务'))}>
        添加任务
      </button>
      <ul>
        {todos.map(todo => (
          <li
            key={todo.id}
            onClick={() => dispatch(toggleTodo(todo.id))}
            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
          >
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;

Redux中间件

中间件提供了一个第三方扩展点,位于dispatch action和到达reducer之间。中间件可以用来记录日志、报告错误、与异步API通信、路由等。

常用中间件

  • redux-thunk: 处理异步action
  • redux-saga: 更复杂的异步流程管理
  • redux-logger: 记录action和状态变化
  • redux-persist: 持久化状态

配置中间件

javascript
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import rootReducer from './reducers';

const middlewares = [thunk];

// 在开发环境添加logger
if (process.env.NODE_ENV === 'development') {
  middlewares.push(logger);
}

// 启用Redux DevTools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(...middlewares))
);

export default store;

使用redux-thunk处理异步操作

javascript
// actions.js
import axios from 'axios';

// 同步action creators
export const fetchTodosRequest = () => ({
  type: 'FETCH_TODOS_REQUEST'
});

export const fetchTodosSuccess = todos => ({
  type: 'FETCH_TODOS_SUCCESS',
  payload: todos
});

export const fetchTodosFailure = error => ({
  type: 'FETCH_TODOS_FAILURE',
  payload: error
});

// 异步action creator (使用thunk)
export const fetchTodos = () => {
  return async dispatch => {
    dispatch(fetchTodosRequest());
    try {
      const response = await axios.get('https://api.example.com/todos');
      dispatch(fetchTodosSuccess(response.data));
    } catch (error) {
      dispatch(fetchTodosFailure(error.message));
    }
  };
};

Redux工具包(Redux Toolkit)

Redux Toolkit是Redux官方推荐的工具集,用于简化Redux开发,减少样板代码。

安装

bash
npm install @reduxjs/toolkit
# 或
yarn add @reduxjs/toolkit

使用createSlice

javascript
import { createSlice } from '@reduxjs/toolkit';

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      // Redux Toolkit允许在reducers中直接"修改"状态
      // 实际上它使用了Immer库来确保不可变更新
      state.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      });
    },
    toggleTodo: (state, action) => {
      const todo = state.find(todo => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    removeTodo: (state, action) => {
      return state.filter(todo => todo.id !== action.payload);
    }
  }
});

// 导出action creators
export const { addTodo, toggleTodo, removeTodo } = todosSlice.actions;

// 导出reducer
export default todosSlice.reducer;

配置Store

javascript
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';
import filtersReducer from './filtersSlice';

const store = configureStore({
  reducer: {
    todos: todosReducer,
    filters: filtersReducer
  }
});

export default store;

使用createAsyncThunk处理异步操作

javascript
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

// 创建异步thunk
export const fetchTodos = createAsyncThunk(
  'todos/fetchTodos',
  async (_, { rejectWithValue }) => {
    try {
      const response = await axios.get('https://api.example.com/todos');
      return response.data;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
    error: null
  },
  reducers: {
    // 其他reducers...
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.payload;
      });
  }
});

export default todosSlice.reducer;

Redux最佳实践

目录结构

推荐使用"特性文件夹"或"Duck模式"组织Redux代码:

src/
  features/
    todos/
      todosSlice.js   // 包含reducer和actions
      TodoList.js     // 组件
      TodoItem.js     // 组件
    filters/
      filtersSlice.js // 包含reducer和actions
      FilterPanel.js  // 组件
  app/
    store.js          // Redux store配置
  App.js
  index.js

性能优化

  1. 避免不必要的渲染:使用React.memouseCallbackuseMemo
jsx
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { toggleTodo } from './todosSlice';

const TodoItem = React.memo(({ id, text, completed }) => {
  const dispatch = useDispatch();
  
  // 使用useCallback缓存回调函数
  const handleToggle = useCallback(() => {
    dispatch(toggleTodo(id));
  }, [dispatch, id]);
  
  return (
    <li
      onClick={handleToggle}
      style={{ textDecoration: completed ? 'line-through' : 'none' }}
    >
      {text}
    </li>
  );
});

export default TodoItem;
  1. 选择性地订阅状态:只订阅组件需要的状态部分
jsx
// 不好的做法:订阅整个状态
const todos = useSelector(state => state);

// 好的做法:只订阅需要的部分
const todos = useSelector(state => state.todos);

// 更好的做法:使用选择器函数
const selectCompletedTodos = state => state.todos.filter(todo => todo.completed);
const completedTodos = useSelector(selectCompletedTodos);
  1. 使用Reselect库进行记忆化选择
javascript
import { createSelector } from '@reduxjs/toolkit';

const selectTodos = state => state.todos;
const selectFilter = state => state.filter;

export const selectVisibleTodos = createSelector(
  [selectTodos, selectFilter],
  (todos, filter) => {
    switch (filter) {
      case 'all':
        return todos;
      case 'completed':
        return todos.filter(todo => todo.completed);
      case 'active':
        return todos.filter(todo => !todo.completed);
      default:
        return todos;
    }
  }
);

常见陷阱

  1. 修改状态:Redux要求reducer是纯函数,不应直接修改状态
javascript
// 错误:直接修改状态
function badReducer(state, action) {
  state.completed = true; // 不要这样做!
  return state;
}

// 正确:返回新状态
function goodReducer(state, action) {
  return {
    ...state,
    completed: true
  };
}
  1. 过度使用Redux:不是所有状态都需要放在Redux中
jsx
// 本地组件状态使用useState
function Counter() {
  // 这种简单的UI状态不需要放在Redux中
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  1. 过度抽象:保持简单,只在需要时添加复杂性

Redux vs 其他状态管理方案

特性ReduxContext APIMobXZustand
学习曲线陡峭平缓中等平缓
样板代码
可预测性
性能良好有限良好良好
开发工具强大基本良好良好
中间件丰富有限
社区支持强大良好良好增长中
适用规模中-大小-中中-大小-中-大

总结

Redux是一个强大的状态管理库,特别适合中大型应用。它提供了可预测的状态管理,丰富的中间件生态系统和强大的开发工具。

主要优点:

  • 可预测的单向数据流
  • 集中式状态管理
  • 强大的开发工具和调试能力
  • 丰富的中间件生态系统
  • 大型社区支持

主要缺点:

  • 学习曲线陡峭
  • 需要编写大量样板代码(可通过Redux Toolkit缓解)
  • 对于小型应用可能过于复杂

随着Redux Toolkit的推出,Redux变得更加易用,减少了样板代码,并提供了更好的开发体验。对于需要复杂状态管理的应用,Redux仍然是一个强大的选择。