Skip to content

Redux Toolkit 深入

现代 Redux:基础使用、Slice/Thunk/RTK Query、核心原理与源码解析

概述

Redux Toolkit(RTK) 是 Redux 官方推荐的工具集,解决了原生 Redux "样板代码太多"的痛点。它内置了 Immer(可变风格更新)、createAsyncThunk(异步)、RTK Query(数据获取)等能力,是大型团队和复杂应用的首选。


一、优势与劣势

优势

  • 官方标准:Redux 团队维护,文档完善,社区庞大,大量企业级项目验证
  • 严格规范:单向数据流 + action + reducer,团队协作时代码风格统一
  • 内置 Immer:在 reducer 中直接修改 state(state.items.push(...)),自动生成不可变更新
  • DevTools 最强:Redux DevTools 支持 action 日志、时间旅行调试、状态快照导入导出
  • RTK Query:内置的数据获取/缓存方案,类似 TanStack Query,与 Redux store 深度集成
  • createAsyncThunk:标准化异步流程(pending/fulfilled/rejected 三种状态)
  • 中间件体系:可扩展的中间件管道(logger、analytics、WebSocket 等)
  • TypeScript 优秀:完整的类型推断,configureStore 自动推断 RootState

劣势

  • 体积最大:gzip 后约 11KB(对比 Zustand ~1KB)
  • 样板代码仍不少:虽然比原生 Redux 少很多,但 slice + thunk + type 定义仍有一定模板
  • 必须 Provider:根组件必须包裹 <Provider store={store}>
  • 学习曲线:需要理解 action、reducer、dispatch、middleware、thunk 等概念
  • 过度架构:小型项目使用 RTK 是过度工程化

二、基础使用

安装

bash
npm install @reduxjs/toolkit react-redux

创建 Slice

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

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    filter: 'all', // 'all' | 'active' | 'done'
  },
  reducers: {
    addTodo: (state, action) => {
      // 直接 push!RTK 内置 Immer
      state.items.push({
        id: Date.now(),
        text: action.payload,
        done: false,
      });
    },
    toggleTodo: (state, action) => {
      const todo = state.items.find(t => t.id === action.payload);
      if (todo) todo.done = !todo.done; // 直接修改
    },
    removeTodo: (state, action) => {
      state.items = state.items.filter(t => t.id !== action.payload);
    },
    setFilter: (state, action) => {
      state.filter = action.payload;
    },
  },
});

// 自动生成 action creators
export const { addTodo, toggleTodo, removeTodo, setFilter } = todosSlice.actions;
export default todosSlice.reducer;

异步 Thunk

jsx
import { createAsyncThunk } from '@reduxjs/toolkit';

// 创建异步 thunk
export const fetchTodos = createAsyncThunk(
  'todos/fetch', // action type 前缀
  async (_, { rejectWithValue }) => {
    try {
      const res = await fetch('/api/todos');
      if (!res.ok) throw new Error('请求失败');
      return await res.json();
    } catch (err) {
      return rejectWithValue(err.message);
    }
  }
);

// 在 slice 中处理异步状态
const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [], loading: false, error: null },
  reducers: { /* ... */ },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload ?? action.error.message;
      });
  },
});

创建 Store

typescript
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';
import userReducer from './userSlice';

const store = configureStore({
  reducer: {
    todos: todosReducer,
    user: userReducer,
  },
  // middleware 默认包含 thunk + serializableCheck + immutableCheck
});

// 类型导出
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// 类型安全的 Hooks
import { useSelector, useDispatch } from 'react-redux';
export const useAppSelector = useSelector.withTypes<RootState>();
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();

在组件中使用

jsx
import { Provider } from 'react-redux';
import { useAppSelector, useAppDispatch } from './store';
import { addTodo, toggleTodo, removeTodo, fetchTodos } from './todosSlice';

// 根组件包裹 Provider
function App() {
  return (
    <Provider store={store}>
      <TodoApp />
    </Provider>
  );
}

function TodoApp() {
  const { items, loading, error } = useAppSelector((state) => state.todos);
  const dispatch = useAppDispatch();

  useEffect(() => {
    dispatch(fetchTodos());
  }, [dispatch]);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;

  return (
    <div>
      <button onClick={() => dispatch(addTodo('新任务'))}>添加</button>
      <ul>
        {items.map(todo => (
          <li key={todo.id}>
            <span
              onClick={() => dispatch(toggleTodo(todo.id))}
              style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
            >
              {todo.text}
            </span>
            <button onClick={() => dispatch(removeTodo(todo.id))}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

三、RTK Query

RTK Query 是 Redux Toolkit 内置的数据获取与缓存方案,类似 TanStack Query 但与 Redux store 深度集成。

typescript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Post', 'User'],
  endpoints: (builder) => ({
    // 查询
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: ['Post'],
    }),
    getPostById: builder.query({
      query: (id) => `/posts/${id}`,
      providesTags: (result, error, id) => [{ type: 'Post', id }],
    }),
    // 变更
    createPost: builder.mutation({
      query: (body) => ({ url: '/posts', method: 'POST', body }),
      invalidatesTags: ['Post'], // 创建后自动重新获取列表
    }),
    deletePost: builder.mutation({
      query: (id) => ({ url: `/posts/${id}`, method: 'DELETE' }),
      invalidatesTags: (result, error, id) => [{ type: 'Post', id }],
    }),
  }),
});

export const {
  useGetPostsQuery,
  useGetPostByIdQuery,
  useCreatePostMutation,
  useDeletePostMutation,
} = api;

// 添加到 store
const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
    // ...其他 reducer
  },
  middleware: (getDefault) => getDefault().concat(api.middleware),
});
jsx
// 组件中使用
function PostList() {
  const { data: posts, isLoading, error } = useGetPostsQuery();
  const [deletePost] = useDeletePostMutation();

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage />;

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          {post.title}
          <button onClick={() => deletePost(post.id)}>删除</button>
        </li>
      ))}
    </ul>
  );
}

四、Selector 与 createSelector

typescript
import { createSelector } from '@reduxjs/toolkit';

// 基础 selector
const selectTodos = (state: RootState) => state.todos.items;
const selectFilter = (state: RootState) => state.todos.filter;

// 记忆化 selector(只有输入变化时才重新计算)
const selectFilteredTodos = createSelector(
  [selectTodos, selectFilter],
  (todos, filter) => {
    switch (filter) {
      case 'active': return todos.filter(t => !t.done);
      case 'done': return todos.filter(t => t.done);
      default: return todos;
    }
  }
);

// 使用
function FilteredList() {
  const filteredTodos = useAppSelector(selectFilteredTodos);
  // filter 或 todos 不变时,selectFilteredTodos 返回缓存结果
}

五、核心原理

Redux 数据流

用户操作 → dispatch(action) → middleware 管道 → reducer → 新 state → 通知订阅者 → 重渲染

action = { type: 'todos/addTodo', payload: '学习 Redux' }

middleware[0] → middleware[1] → ... → reducer
    (thunk)      (logger)          (纯函数)

                              newState = { todos: [...] }

                              store.getState() 返回新 state

                              useSelector 比较 → 触发重渲染

createSlice 原理

javascript
// createSlice 做了什么?
function createSlice({ name, initialState, reducers }) {
  const actionCreators = {};
  const caseReducers = {};

  for (const [key, reducer] of Object.entries(reducers)) {
    const type = `${name}/${key}`; // 如 'todos/addTodo'
    actionCreators[key] = (payload) => ({ type, payload });
    caseReducers[type] = reducer;
  }

  // 最终的 reducer(包裹 Immer produce)
  const reducer = produce((draft, action) => {
    const caseReducer = caseReducers[action.type];
    if (caseReducer) {
      return caseReducer(draft, action);
    }
  }, initialState);

  return { actions: actionCreators, reducer };
}

Immer 如何工作

javascript
import { produce } from 'immer';

// produce 的简化原理:
// 1. 创建 state 的 Proxy 代理(draft)
// 2. 在 draft 上记录所有修改操作
// 3. 根据修改记录生成新的不可变对象

const nextState = produce(currentState, (draft) => {
  draft.todos.push({ text: '新任务' }); // 看起来是修改
});

// 实际:currentState 未变,nextState 是新对象
// nextState !== currentState
// nextState.todos !== currentState.todos
// 但未修改的部分共享引用:nextState.user === currentState.user

useSelector 的重渲染机制

javascript
function useSelector(selector) {
  const store = useContext(ReduxContext);

  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState())
  );
  // store 变化 → 调用 selector → 与上次结果比较(===)
  // 相同 → 跳过渲染
  // 不同 → 触发渲染
}

六、最佳实践

项目结构(Feature-based)

src/
├── features/
│   ├── todos/
│   │   ├── todosSlice.ts      # slice + thunk + selector
│   │   ├── TodoList.tsx        # 组件
│   │   └── todosApi.ts         # RTK Query endpoints
│   └── user/
│       ├── userSlice.ts
│       └── UserProfile.tsx
├── app/
│   ├── store.ts               # configureStore
│   └── hooks.ts               # useAppSelector / useAppDispatch
└── services/
    └── api.ts                 # createApi base

面试高频题

Q: Redux Toolkit 比原生 Redux 好在哪?

  1. createSlice 合并了 action type + action creator + reducer,减少 60%+ 样板代码
  2. 内置 Immer:reducer 中直接"修改" state,无需手写展开运算符
  3. configureStore:自动配置 DevTools、thunk 中间件、序列化检查
  4. createAsyncThunk:标准化异步流程,自动 dispatch pending/fulfilled/rejected
  5. RTK Query:内置数据获取缓存,替代 saga/thunk 中手写的 API 逻辑

Q: Redux 中间件是什么?工作原理?

中间件是 dispatch → reducer 之间的拦截层,形成管道。每个中间件接收 (store) => (next) => (action) => { ... } 的柯里化函数。可以拦截、修改、延迟 action,也可以发起副作用。常见的 thunk 中间件就是检测 action 是否是函数,是则执行它。

Q: 什么时候选 Redux Toolkit 而不是 Zustand?

  • 大型团队需要严格规范和统一的架构模式
  • 需要强大的 DevTools(时间旅行、action 日志)
  • 已有 Redux 代码需要渐进式升级
  • 需要 RTK Query 替代手写的 API 层
  • 需要中间件管道处理复杂副作用(如 WebSocket、日志分析)