Redux 教程

1. 项目概述

Redux 是 JavaScript 应用的可预测状态容器,它可以帮助你编写行为一致、可预测且易于测试的应用。Redux 主要用于管理应用的状态,特别是在大型应用中,它可以使状态管理变得更加可预测和可维护。

1.1 主要特性

  • 单一数据源:整个应用的状态存储在一个单一的 store 中
  • 状态是只读的:唯一改变状态的方法是触发 action
  • 使用纯函数来执行修改:通过 reducer 纯函数来处理状态更新
  • 可预测性:相同的输入总是产生相同的输出
  • 可测试性:纯函数易于测试
  • 中间件支持:通过中间件处理异步操作和副作用
  • 时间旅行调试:可以回溯和重放状态变化
  • 生态系统丰富:有大量的插件和工具

1.2 适用场景

  • 大型前端应用
  • 状态逻辑复杂的应用
  • 需要可预测状态管理的应用
  • 需要时间旅行调试的应用
  • 多组件共享状态的应用
  • 服务器端渲染的应用

2. 安装与设置

2.1 环境要求

  • Node.js 14.0 或更高版本
  • npm 或 yarn 包管理器
  • 基本的 JavaScript 知识

2.2 安装步骤

方法一:在现有项目中安装

# 安装 Redux 核心库
npm install redux

# 如果你使用 React,还需要安装 React-Redux
npm install react-redux

# 如果你需要处理异步操作,还需要安装 Redux-Thunk
npm install redux-thunk

# 如果你需要使用 Redux DevTools,还需要安装
npm install --save-dev redux-devtools-extension

方法二:使用 Create React App

# 创建带有 Redux 的 React 应用
npx create-react-app my-redux-app --template redux

# 进入项目目录
cd my-redux-app

# 启动开发服务器
npm start

2.3 基本项目结构

my-redux-app/
├── src/
│   ├── app/
│   │   └── store.js       # Redux store 配置
│   ├── features/
│   │   └── counter/
│   │       ├── Counter.js # 组件
│   │       └── counterSlice.js # Redux slice
│   ├── index.js           # 应用入口
│   └── App.js             # 根组件
├── package.json           # 项目配置
└── package-lock.json      # 依赖锁定

3. 核心概念

3.1 Action

Action 是一个带有 type 字段的普通 JavaScript 对象,它描述了发生了什么。

// 基本 action
const increment = {
  type: 'counter/increment'
};

// 带 payload 的 action
const addTodo = {
  type: 'todos/addTodo',
  payload: 'Learn Redux'
};

3.2 Reducer

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

// 基本 reducer
function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'counter/increment':
      return state + 1;
    case 'counter/decrement':
      return state - 1;
    default:
      return state;
  }
}

3.3 Store

Store 是保存应用状态的对象,它提供了几个方法来访问和修改状态。

// 创建 store
import { createStore } from 'redux';
import counterReducer from './reducers/counterReducer';

const store = createStore(counterReducer);

// 获取状态
const state = store.getState();

// 分发 action
store.dispatch({ type: 'counter/increment' });

// 订阅状态变化
store.subscribe(() => {
  console.log('State changed:', store.getState());
});

3.4 Dispatch

Dispatch 是 store 的一个方法,用于分发 action 来修改状态。

store.dispatch({ type: 'counter/increment' });

3.5 Subscribe

Subscribe 是 store 的一个方法,用于订阅状态变化,当状态发生变化时执行回调函数。

const unsubscribe = store.subscribe(() => {
  console.log('State changed:', store.getState());
});

// 取消订阅
unsubscribe();

3.6 Middleware

Middleware 是 Redux 的扩展点,用于处理异步操作、日志记录等副作用。

// 日志中间件
const loggerMiddleware = store => next => action => {
  console.log('Dispatching:', action);
  const result = next(action);
  console.log('Next state:', store.getState());
  return result;
};

4. 基本使用

4.1 创建 Redux Store

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

const store = createStore(rootReducer);

export default store;

4.2 创建 Reducer

// src/reducers/index.js
import { combineReducers } from 'redux';
import counterReducer from './counterReducer';
import todosReducer from './todosReducer';

const rootReducer = combineReducers({
  counter: counterReducer,
  todos: todosReducer
});

export default rootReducer;
// src/reducers/counterReducer.js
const initialState = 0;

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'counter/increment':
      return state + 1;
    case 'counter/decrement':
      return state - 1;
    case 'counter/reset':
      return 0;
    case 'counter/incrementByAmount':
      return state + action.payload;
    default:
      return state;
  }
}

export default counterReducer;

4.3 创建 Action Creators

// src/actions/counterActions.js
export const increment = () => ({
  type: 'counter/increment'
});

export const decrement = () => ({
  type: 'counter/decrement'
});

export const reset = () => ({
  type: 'counter/reset'
});

export const incrementByAmount = (amount) => ({
  type: 'counter/incrementByAmount',
  payload: amount
});

4.4 在 React 中使用 Redux

使用 connect 函数

// src/components/Counter.js
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement, reset, incrementByAmount } from '../actions/counterActions';

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

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

const mapDispatchToProps = {
  increment,
  decrement,
  reset,
  incrementByAmount
};

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

使用 Hooks

// src/components/Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, reset, incrementByAmount } from '../actions/counterActions';

function Counter() {
  const count = useSelector(state => state.counter);
  const dispatch = useDispatch();

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
      <button onClick={() => dispatch(reset())}>Reset</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>Increment by 5</button>
    </div>
  );
}

export default Counter;

4.5 提供 Store

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

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

5. 高级功能

5.1 异步操作

使用 Redux-Thunk

// src/actions/todoActions.js
import axios from 'axios';

export const fetchTodos = () => async (dispatch) => {
  dispatch({ type: 'todos/fetchTodosStart' });
  
  try {
    const response = await axios.get('https://jsonplaceholder.typicode.com/todos');
    dispatch({ 
      type: 'todos/fetchTodosSuccess', 
      payload: response.data 
    });
  } catch (error) {
    dispatch({ 
      type: 'todos/fetchTodosError', 
      payload: error.message 
    });
  }
};

配置 Redux-Thunk

// src/app/store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

export default store;

5.2 Redux Toolkit

Redux Toolkit 是官方推荐的 Redux 开发工具集,它简化了 Redux 的使用。

安装 Redux Toolkit

npm install @reduxjs/toolkit

创建 Slice

// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

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

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    }
  }
});

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

export default counterSlice.reducer;

配置 Store

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todosReducer from '../features/todos/todosSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    todos: todosReducer
  }
});

5.3 选择器

选择器是用于从 Redux 状态中提取数据的函数,它可以帮助你避免重复的状态选择逻辑。

// src/features/todos/todosSlice.js
import { createSlice, createSelector } from '@reduxjs/toolkit';

// ... slice 代码 ...

// 选择器
export const selectAllTodos = (state) => state.todos.items;
export const selectCompletedTodos = createSelector(
  [selectAllTodos],
  (todos) => todos.filter(todo => todo.completed)
);
export const selectTodoById = (state, todoId) => 
  state.todos.items.find(todo => todo.id === todoId);

使用选择器

// src/components/TodoList.js
import React from 'react';
import { useSelector } from 'react-redux';
import { selectAllTodos, selectCompletedTodos } from '../features/todos/todosSlice';

function TodoList() {
  const todos = useSelector(selectAllTodos);
  const completedTodos = useSelector(selectCompletedTodos);

  return (
    <div>
      <h2>All Todos ({todos.length})</h2>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
      <h2>Completed Todos ({completedTodos.length})</h2>
      <ul>
        {completedTodos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;

5.4 中间件

创建自定义中间件

// src/middleware/logger.js
export const loggerMiddleware = store => next => action => {
  console.log('Dispatching:', action);
  const result = next(action);
  console.log('Next state:', store.getState());
  return result;
};

使用中间件

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import { loggerMiddleware } from '../middleware/logger';

export const store = configureStore({
  reducer: {
    counter: counterReducer
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(loggerMiddleware)
});

5.5 持久化

安装 Redux Persist

npm install redux-persist

配置持久化

// src/app/store.js
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import counterReducer from '../features/counter/counterSlice';

const persistConfig = {
  key: 'root',
  storage,
};

const persistedReducer = persistReducer(persistConfig, counterReducer);

export const store = configureStore({
  reducer: {
    counter: persistedReducer
  },
  middleware: getDefaultMiddleware({
    serializableCheck: false
  })
});

export const persistor = persistStore(store);

使用 PersistGate

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './app/store';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

6. 实用案例

6.1 待办事项应用

创建 Todo Slice

// src/features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

export const fetchTodos = createAsyncThunk(
  'todos/fetchTodos',
  async () => {
    const response = await axios.get('https://jsonplaceholder.typicode.com/todos');
    return response.data;
  }
);

const initialState = {
  items: [],
  status: 'idle',
  error: null
};

export const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTodo: (state, action) => {
      state.items.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      });
    },
    toggleTodo: (state, action) => {
      const todo = state.items.find(todo => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    deleteTodo: (state, action) => {
      state.items = state.items.filter(todo => todo.id !== action.payload);
    }
  },
  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.error.message;
      });
  }
});

export const { addTodo, toggleTodo, deleteTodo } = todosSlice.actions;

export default todosSlice.reducer;

创建 Todo 组件

// src/components/TodoList.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { 
  fetchTodos, 
  addTodo, 
  toggleTodo, 
  deleteTodo 
} from '../features/todos/todosSlice';

function TodoList() {
  const dispatch = useDispatch();
  const { items: todos, status, error } = useSelector(state => state.todos);
  const [inputText, setInputText] = React.useState('');

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchTodos());
    }
  }, [status, dispatch]);

  const handleAddTodo = () => {
    if (inputText.trim()) {
      dispatch(addTodo(inputText));
      setInputText('');
    }
  };

  return (
    <div>
      <h1>Todo List</h1>
      
      <div>
        <input
          type="text"
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
          placeholder="Add a todo"
        />
        <button onClick={handleAddTodo}>Add</button>
      </div>

      {status === 'loading' && <p>Loading...</p>}
      {status === 'failed' && <p>Error: {error}</p>}

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch(toggleTodo(todo.id))}
            />
            <span style={{
              textDecoration: todo.completed ? 'line-through' : 'none'
            }}>
              {todo.text}
            </span>
            <button onClick={() => dispatch(deleteTodo(todo.id))}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;

6.2 计数器应用

创建 Counter Slice

// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  value: 0
};

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
    reset: (state) => {
      state.value = 0;
    }
  }
});

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

export default counterSlice.reducer;

创建 Counter 组件

// src/components/Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount, reset } from '../features/counter/counterSlice';

function Counter() {
  const count = useSelector(state => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
      <button onClick={() => dispatch(reset())}>Reset</button>
      <button onClick={() => dispatch(incrementByAmount(10))}>Increment by 10</button>
    </div>
  );
}

export default Counter;

7. 性能优化

7.1 避免不必要的渲染

  • 使用 useSelector 的正确方式:使用选择器函数而不是对象解构
  • 使用 createSelector:创建记忆化的选择器
  • 使用 React.memo:包装组件以避免不必要的渲染
  • 使用 batch:批量处理多个 dispatch

7.2 状态规范化

  • 规范化状态结构:使用 ID 作为键,而不是数组
  • 避免深层嵌套:保持状态结构扁平化
  • 使用实体适配器:使用 Redux Toolkit 的 createEntityAdapter

7.3 异步操作优化

  • 使用 createAsyncThunk:处理异步操作
  • 避免重复请求:使用状态来跟踪请求状态
  • 使用缓存:缓存频繁使用的数据

7.4 代码分割

  • 使用动态导入:按需加载 Redux 模块
  • 使用 Redux Injectors:动态注入 reducers

8. 最佳实践

8.1 项目结构

  • 按功能组织:将相关的 reducer、action 和组件放在一起
  • 使用 ducks 模式:将 action、reducer 和选择器放在一个文件中
  • 使用 Redux Toolkit:使用官方推荐的工具集

8.2 Action 设计

  • 使用领域特定的 action 类型:如 counter/increment
  • 使用 payload 属性:传递额外的数据
  • 使用 action creators:创建 action 的函数
  • 保持 action 简洁:每个 action 只做一件事

8.3 Reducer 设计

  • 使用纯函数:不修改输入状态,返回新状态
  • 使用默认参数:设置初始状态
  • 处理所有 action:使用 default 分支
  • 使用 Immer:通过 Redux Toolkit 使用 Immer 简化状态更新

8.4 中间件使用

  • 使用 Redux Thunk:处理异步操作
  • 使用 Redux Saga:处理复杂的异步流程
  • 使用 Redux Observable:使用 RxJS 处理异步操作
  • 使用日志中间件:开发时使用

8.5 测试

  • 测试 action creators:测试返回正确的 action
  • 测试 reducers:测试状态更新
  • 测试选择器:测试数据提取
  • 测试异步操作:测试异步流程
  • 测试组件:测试组件与 Redux 的交互

9. 常见问题与解决方案

9.1 状态更新不生效

问题:dispatch action 后状态没有更新

解决方案

  • 检查 reducer 是否返回新状态
  • 检查 action type 是否正确
  • 检查 reducer 是否处理了该 action
  • 检查是否使用了纯函数

9.2 组件不重新渲染

问题:状态更新后组件没有重新渲染

解决方案

  • 检查 useSelector 是否正确使用
  • 检查是否返回了新的引用
  • 检查组件是否被 React.memo 包装
  • 检查是否有不必要的依赖

9.3 异步操作错误

问题:异步操作失败或不执行

解决方案

  • 检查中间件是否正确配置
  • 检查异步 action creator 是否正确
  • 检查网络请求是否正确
  • 检查错误处理是否完善

9.4 状态结构过于复杂

问题:状态结构深层嵌套,难以管理

解决方案

  • 规范化状态结构
  • 使用实体适配器
  • 分解 reducer
  • 使用 combineReducers

9.5 Redux DevTools 不工作

问题:Redux DevTools 无法连接或显示状态

解决方案

  • 检查是否安装了 redux-devtools-extension
  • 检查 store 配置是否正确
  • 检查浏览器是否安装了 Redux DevTools 扩展
  • 检查是否有语法错误

10. 参考资源

10.1 官方文档

10.2 学习资源

10.3 工具与库

10.4 示例项目

11. 总结

Redux 是一个强大的状态管理库,它通过单一数据源、纯函数和可预测的状态更新,为 JavaScript 应用提供了一种一致的状态管理方案。通过本教程,你应该已经掌握了 Redux 的基本使用方法和高级功能,包括:

  • 核心概念(action、reducer、store)
  • 基本使用
  • 异步操作
  • Redux Toolkit
  • 选择器
  • 中间件
  • 持久化
  • 性能优化
  • 最佳实践

Redux 的设计理念是 "可预测的状态管理",它通过严格的规则和模式,使得状态管理变得更加可预测和可维护。虽然 Redux 有一定的学习曲线,但是一旦掌握了它的核心概念和最佳实践,它将成为你开发大型应用的得力助手。

Redux Toolkit 的出现大大简化了 Redux 的使用,它提供了一套开箱即用的工具,使得 Redux 的开发变得更加高效和愉快。官方推荐使用 Redux Toolkit 来开发 Redux 应用。

Redux 适合从简单的个人项目到复杂的企业级应用的各种场景。它的生态系统非常丰富,有大量的插件和工具,可以满足各种需求。

随着前端技术的不断发展,Redux 也在不断演进,提供了更多的特性和优化。希望本教程对你的学习和开发有所帮助!

« 上一篇 Webpack 教程 - 现代 JavaScript 应用的静态模块打包器 下一篇 » Zustand 教程 - 轻量级状态管理库