第 13 章

Redux Toolkit:企业级状态管理完全指南

第13章:Redux Toolkit:企业级状态管理完全指南

Redux 的核心理念是正确的:单一数据源、纯函数 reducer、不可变更新。RTK 的使命是消灭这些原则带来的样板代码。

本章核心问题:RTK 如何消灭原始 Redux 的样板代码?RTK Query 解决了什么问题? 读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

为什么原始 Redux 让人痛苦

Redux 的核心理念是正确的:单一数据源、纯函数 reducer、不可变更新。这些原则在大型团队中提供了真正的价值——可预测性、可测试性、时间旅行调试。但原始 Redux 的人体工程学是一场噩梦。

写一个简单的计数器功能,你需要:

// 原始 Redux 的仪式感(2018 年的写法)
// 1. 定义 Action Types 常量(避免拼写错误)
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';
const INCREMENT_BY_AMOUNT = 'counter/INCREMENT_BY_AMOUNT';

// 2. 定义 Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const incrementByAmount = (amount: number) => ({
  type: INCREMENT_BY_AMOUNT,
  payload: amount,
});

// 3. 定义 State 类型
interface CounterState {
  value: number;
}

const initialState: CounterState = { value: 0 };

// 4. 写 Reducer(手动不可变更新)
function counterReducer(
  state: CounterState = initialState,
  action: { type: string; payload?: number }
): CounterState {
  switch (action.type) {
    case INCREMENT:
      return { ...state, value: state.value + 1 };
    case DECREMENT:
      return { ...state, value: state.value - 1 };
    case INCREMENT_BY_AMOUNT:
      return { ...state, value: state.value + (action.payload ?? 0) };
    default:
      return state;
  }
}

// 5. 异步操作需要 redux-thunk 或 redux-saga 额外配置
// 6. Selector 需要手动编写
// 7. 单元测试每个 action creator 和 reducer...

这是 5 个文件、数百行样板代码,只为实现一个计数器。Redux Toolkit(RTK)的使命就是消灭这些样板,同时保留 Redux 的所有原则。

createSlice:把 Action Creators 和 Reducer 合为一体

RTK 最核心的 API 是 createSlice。它的名字来自"状态切片"的概念——你把全局状态切分成领域片段(counter、user、cart),每个 slice 包含该领域的初始状态、reducer 函数和自动生成的 action creators。

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

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

const counterSlice = createSlice({
  name: 'counter',                    // 生成 action type 的前缀:'counter/increment'
  initialState: {
    value: 0,
    status: 'idle',
  } as CounterState,
  reducers: {
    // RTK 内置 immer:可以直接 mutate state!
    increment(state) {
      state.value += 1;               // 直接修改,immer 生成不可变更新
    },
    decrement(state) {
      state.value -= 1;
    },
    incrementByAmount(state, action: PayloadAction<number>) {
      state.value += action.payload;  // PayloadAction<T> 提供类型安全的 payload
    },
    reset(state) {
      state.value = 0;
    },
  },
});

// 自动生成的 action creators:
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
// counterSlice.actions.increment() 返回 { type: 'counter/increment' }

// Reducer 直接导出,注册到 store
export default counterSlice.reducer;

原来需要 60+ 行的代码现在是 25 行,且类型完全安全。但 createSlice 的真正价值不只是代码量——它消除了 action types 字符串拼写错误这类运行时 bug,因为 action creators 是直接从 reducer 函数名自动派生的。

immer 集成的深层含义

RTK 内置 immer 的决定不只是便利性考量。它解决了 Redux 最大的人为错误来源之一:

// 原始 Redux 的常见 Bug:直接 mutate state(违反不可变性但不报错)
function buggyReducer(state = initialState, action) {
  if (action.type === 'addItem') {
    state.items.push(action.payload); // 直接修改!Redux 不报错,但状态追踪失效
    return state; // 返回同一引用,React 不知道状态变了
  }
  return state;
}

// RTK + immer:直接 mutate 是正确的做法
const itemsSlice = createSlice({
  name: 'items',
  initialState: { list: [] as string[] },
  reducers: {
    addItem(state, action: PayloadAction<string>) {
      state.list.push(action.payload); // 这里的 state 是 immer Proxy draft
      // immer 在 reducer 执行完后生成新的不可变对象
    },
  },
});

Level 2 · 它是怎么运行的(3-5年经验)

configureStore:设置 Redux Store

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import userReducer from './userSlice';
import cartReducer from './cartSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    user: userReducer,
    cart: cartReducer,
  },
  // configureStore 自动添加:
  // - redux-thunk middleware(异步操作)
  // - Redux DevTools Extension 集成
  // - 开发环境的不可变性检查
  // - 开发环境的序列化检查
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ['user/setToken'], // 某些 action 包含不可序列化数据
      },
    }),
});

// 导出 RootState 和 AppDispatch 类型(整个项目共享)
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// 类型安全的 Hook(推荐创建 typed hooks 文件)
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

createAsyncThunk:优雅处理异步操作

Redux 的异步处理历来是痛点。redux-thunk 提供了基础能力,但手动处理 loading/success/error 三态需要大量样板代码。createAsyncThunk 把这个模式标准化了:

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

// 定义异步 thunk:
// 第一个参数:action type 前缀
// 第二个参数:async payload creator 函数
export const fetchUserById = createAsyncThunk(
  'user/fetchById',
  async (userId: string, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) {
        return rejectWithValue(`HTTP error ${response.status}`);
      }
      return response.json() as Promise<User>;
    } catch (error) {
      return rejectWithValue('Network error');
    }
  }
);

// createAsyncThunk 自动生成三个 action:
// fetchUserById.pending   → 'user/fetchById/pending'
// fetchUserById.fulfilled → 'user/fetchById/fulfilled'
// fetchUserById.rejected  → 'user/fetchById/rejected'

interface UserState {
  user: User | null;
  status: 'idle' | 'loading' | 'succeeded' | 'failed';
  error: string | null;
}

const userSlice = createSlice({
  name: 'user',
  initialState: {
    user: null,
    status: 'idle',
    error: null,
  } as UserState,
  reducers: {
    // 同步 reducer
    logout(state) {
      state.user = null;
      state.status = 'idle';
    },
  },
  // extraReducers 处理来自 thunk 的 action
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.status = 'loading';
        state.error = null;
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.user = action.payload; // 类型推断:action.payload 是 User
      })
      .addCase(fetchUserById.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.payload as string ?? 'Unknown error';
      });
  },
});

// 在组件中使用
function UserProfile({ userId }: { userId: string }) {
  const dispatch = useAppDispatch();
  const { user, status, error } = useAppSelector(state => state.user);

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

  if (status === 'loading') return <Spinner />;
  if (status === 'failed') return <Error message={error} />;
  if (!user) return null;
  return <div>{user.name}</div>;
}

Level 3 · 规范怎么定义的(资深)

RTK Query:数据获取与缓存的完整解决方案

RTK Query 是 RTK 2.0 的旗舰特性,它将数据获取提升到完全不同的抽象层次。如果你用过 React Query,RTK Query 的概念会很熟悉——但它深度集成到 Redux,与全局状态管理无缝协作。

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

// 定义 API:一个 API 对应一个后端服务
export const userApi = createApi({
  reducerPath: 'userApi',     // 在 Redux store 中的挂载路径
  baseQuery: fetchBaseQuery({
    baseUrl: '/api',
    prepareHeaders: (headers, { getState }) => {
      // 从 Redux state 中读取 token 注入请求头
      const token = (getState() as RootState).auth.token;
      if (token) headers.set('authorization', `Bearer ${token}`);
      return headers;
    },
  }),
  tagTypes: ['User', 'Post'],  // 用于缓存失效的标签系统
  endpoints: (builder) => ({
    // Query:读操作,自动缓存
    getUser: builder.query<User, string>({
      query: (userId) => `/users/${userId}`,
      providesTags: (result, error, userId) => [{ type: 'User', id: userId }],
    }),
    getUserPosts: builder.query<Post[], string>({
      query: (userId) => `/users/${userId}/posts`,
      providesTags: (result, error, userId) =>
        result
          ? [...result.map(p => ({ type: 'Post' as const, id: p.id })),
             { type: 'Post', id: 'LIST' }]
          : [{ type: 'Post', id: 'LIST' }],
    }),
    // Mutation:写操作,触发缓存失效
    updateUser: builder.mutation<User, { id: string; changes: Partial<User> }>({
      query: ({ id, changes }) => ({
        url: `/users/${id}`,
        method: 'PATCH',
        body: changes,
      }),
      invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
      // Optimistic update:乐观更新
      async onQueryStarted({ id, changes }, { dispatch, queryFulfilled }) {
        const patchResult = dispatch(
          userApi.util.updateQueryData('getUser', id, (draft) => {
            Object.assign(draft, changes); // immer draft
          })
        );
        try {
          await queryFulfilled;
        } catch {
          patchResult.undo(); // 请求失败时回滚
        }
      },
    }),
  }),
});

// RTK Query 自动生成 React Hooks
export const {
  useGetUserQuery,
  useGetUserPostsQuery,
  useUpdateUserMutation,
} = userApi;

在组件中使用 RTK Query

function UserProfile({ userId }: { userId: string }) {
  // useGetUserQuery 自动处理:loading/error/data/refetch/polling
  const {
    data: user,
    isLoading,
    isError,
    error,
  } = useGetUserQuery(userId, {
    pollingInterval: 30000,      // 每 30 秒自动刷新
    skip: !userId,               // userId 为空时跳过请求
    refetchOnMountOrArgChange: true,
  });

  const [updateUser, { isLoading: isUpdating }] = useUpdateUserMutation();

  const handleUpdateName = async (name: string) => {
    try {
      await updateUser({ id: userId, changes: { name } }).unwrap();
      // unwrap() 在请求失败时抛出异常
    } catch (error) {
      console.error('Update failed:', error);
    }
  };

  if (isLoading) return <Skeleton />;
  if (isError) return <ErrorBoundary error={error} />;
  return (
    <div>
      <h1>{user?.name}</h1>
      <button
        onClick={() => handleUpdateName('New Name')}
        disabled={isUpdating}
      >
        Update Name
      </button>
    </div>
  );
}

缓存失效与 Tag 系统

RTK Query 的 tag 系统是其最聪明的设计之一。当你定义 providesTagsinvalidatesTags 时,RTK Query 自动管理缓存失效:

这消除了手动管理缓存状态的大量代码。

Entity Adapters:规范化状态管理

当你需要管理列表数据(用户列表、文章列表)时,createEntityAdapter 提供了标准化的 CRUD 操作和选择器:

import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';

interface Post {
  id: string;
  title: string;
  authorId: string;
  createdAt: string;
}

const postsAdapter = createEntityAdapter<Post>({
  // 默认使用 entity.id,可以自定义
  sortComparer: (a, b) => b.createdAt.localeCompare(a.createdAt), // 按时间倒序
});

const postsSlice = createSlice({
  name: 'posts',
  initialState: postsAdapter.getInitialState({
    status: 'idle' as 'idle' | 'loading' | 'succeeded',
  }),
  reducers: {
    // Adapter 提供预构建的 CRUD reducer
    postAdded: postsAdapter.addOne,
    postsReceived: postsAdapter.setAll,
    postUpdated: postsAdapter.updateOne,
    postDeleted: postsAdapter.removeOne,
  },
  extraReducers: (builder) => {
    builder.addCase(fetchPosts.fulfilled, (state, action) => {
      state.status = 'succeeded';
      postsAdapter.setAll(state, action.payload);
    });
  },
});

// Adapter 自动生成类型安全的 selector
const {
  selectAll: selectAllPosts,
  selectById: selectPostById,
  selectIds: selectPostIds,
  selectEntities: selectPostEntities,
  selectTotal: selectPostCount,
} = postsAdapter.getSelectors<RootState>(state => state.posts);

// 规范化状态:{ ids: ['1','2','3'], entities: { '1': {...}, '2': {...} } }
// 按 ID 查找 O(1),无需遍历数组

Redux 是正确选择的时机

有了 Zustand、Jotai 这些更轻量的替代品,Redux 不再是默认选择。以下场景中,Redux 的综合优势才真正体现:

场景 1:大型团队,需要严格的架构约束 Redux 的显式 action → reducer 数据流,加上 Redux DevTools 的完整行为记录,让新成员理解状态变化有统一的追踪路径。10 人以上的团队在 Redux 的显式协议下沟通成本更低。

场景 2:复杂的跨领域状态交互 当 cart 状态依赖 user 状态,当 notification 需要监听 payment 完成,当多个领域的状态需要在单一事务中原子性更新时,Redux 的集中式 store 和 extraReducers 的跨 slice 监听机制是最清晰的解决方案。

场景 3:时间旅行调试是硬性需求 金融应用、电商结账流程、复杂的表单向导——这些场景中,能够"回放"用户操作序列来重现 bug 的价值不可替代。Redux DevTools 的时间旅行是其他方案无法提供的能力。

场景 4:已有 RTK Query 集成需求 如果你需要复杂的数据获取缓存、自动失效、乐观更新、接口 Mock——RTK Query 在 Redux 生态内提供了完整的解决方案,而不需要额外引入 React Query(尽管两者都是优秀的库)。

Redux Toolkit 是 Redux 哲学的现代化呈现:它没有改变 Redux 的核心约束,而是用正确的抽象消灭了那些约束带来的摩擦。这正是好的框架升级应该做的事。


Level 4 · 边界与陷阱(所有人)

生产环境常见问题

在实际项目中,本章涵盖的概念最常见的问题包括:

  1. 忽视边界条件:在正常路径下工作正常的代码,在异常路径(网络失败、用户快速操作、组件卸载)下可能产生 bug。始终考虑清理和取消逻辑。

  2. 过早优化:在没有测量的情况下添加优化代码,增加了复杂度但不一定提升性能。先用 Profiler 确认问题存在,再实施优化。

  3. 文档与实际行为的差异:React 的行为在不同版本间可能有微妙差异,尤其是并发模式相关的特性。始终以实际测试结果为准。

本章评分
4.7  / 5  (23 评分)

💬 留言讨论