Redux Toolkit

深入理解 Redux Toolkit(RTK):createSlice、createAsyncThunk、RTK Query 的用法,以及 Redux Toolkit 如何简化 Redux 开发。

Redux Toolkit

Redux Toolkit 概述

Redux Toolkit(RTK)是 Redux 官方推荐的状态管理方案,它大幅简化了 Redux 的使用方式。在 RTK 出现之前,Redux 的 boilerplate 代码让很多开发者望而却步;RTK 通过一系列内置工具,让 Redux 开发变得现代化和简单。

RTK 的设计理念是「 batteries included」——提供开箱即用的最佳实践,包括:

  • 不可变更新逻辑
  • 异步逻辑处理
  • 数据获取缓存
  • 开发者工具集成

createSlice

createSlice 是 RTK 的核心 API,它自动生成 reducer 和 action creators:

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

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
    status: 'idle'
  },
  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;

关键点:RTK 的 Immer 允许「可变」语法写不可变更新!

createSlicereducers 中,你可以直接修改 state,RTK 会自动处理不可变性:

// 这段代码看起来是直接修改 state
// 但 RTK 会自动转换成不可变更新
reducers: {
  increment: (state) => {
    state.value += 1;
  }
}

// 实际等价于(传统 Redux)
increment: (state, action) => {
  return { ...state, value: state.value + 1 };
}

配置 Store

configureStore

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

const store = configureStore({
  reducer: {
    counter: counterReducer,
    user: userReducer,
  }
});

export default store;

configureStore 自动:

  • 设置 Redux DevTools
  • 设置默认 middleware(包括 serializableCheck 和 thunk)
  • 合并多个 reducer

传统 Redux 对比

// 传统 Redux(需要手动配置)
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';

const rootReducer = combineReducers({
  counter: counterReducer,
  user: userReducer
});

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

React Redux Hooks

useSelector 和 useDispatch

import { useSelector, useDispatch } from 'react-redux';
import { increment } from './counterSlice';

function Counter() {
  // 自动类型推导
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>
        Increment
      </button>
    </div>
  );
}

性能优化:稳定的 selector

function UserProfile() {
  // 每次渲染都创建新对象!会导致不必要的重新渲染
  const userData = useSelector(state => ({
    name: state.user.name,
    email: state.user.email
  }));

  // 更好的做法:拆分 selector
  const userName = useSelector(state => state.user.name);
  const userEmail = useSelector(state => state.user.email);

  // 或者使用 createSelector(来自 reselect)
  const userData = useSelector(state => selectUserData(state));
}

createAsyncThunk

createAsyncThunk 用于处理异步逻辑(如 API 调用):

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

// createAsyncThunk 自动生成 pending/fulfilled/rejected action types
export const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId, { rejectWithValue }) => {
    try {
      const response = await userAPI.getUser(userId);
      return response.data;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const usersSlice = createSlice({
  name: 'users',
  initialState: {
    entities: {},
    loading: 'idle'
  },
  reducers: {
    // 同步 reducers
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.loading = 'pending';
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.entities[action.payload.id] = action.payload;
        state.loading = 'succeeded';
      })
      .addCase(fetchUserById.rejected, (state, action) => {
        state.loading = 'failed';
        state.error = action.payload;
      });
  }
});

使用异步 thunk

function UserProfile({ userId }) {
  const dispatch = useDispatch();
  const user = useSelector(state => state.users.entities[userId]);
  const loading = useSelector(state => state.users.loading);

  useEffect(() => {
    if (loading === 'idle') {
      dispatch(fetchUserById(userId));
    }
  }, [dispatch, userId, loading]);

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

RTK Query

RTK Query 是 RTK 提供的数据获取和缓存解决方案,专为 React 设计:

定义 API

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

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getUser: builder.query({
      query: (userId) => `users/${userId}`,
    }),
    getPosts: builder.query({
      query: () => 'posts',
    }),
    createPost: builder.mutation({
      query: (newPost) => ({
        url: 'posts',
        method: 'POST',
        body: newPost,
      }),
    }),
  }),
});

export const { useGetUserQuery, useGetPostsQuery, useCreatePostMutation } = api;

使用 Hooks

function UserProfile({ userId }) {
  // 自动管理 loading/error/data 状态
  const { data: user, isLoading, isError } = useGetUserQuery(userId);

  if (isLoading) return <Spinner />;
  if (isError) return <Error />;
  return <div>{user.name}</div>;
}

function PostList() {
  const { data: posts } = useGetPostsQuery();

  const [createPost, { isLoading: isCreating }] = useCreatePostMutation();

  return (
    <div>
      <button onClick={() => createPost({ title: 'New Post' })} disabled={isCreating}>
        {isCreating ? 'Creating...' : 'Create Post'}
      </button>
      {posts?.map(post => <PostItem key={post.id} post={post} />)}
    </div>
  );
}

RTK Query 自动做的事情

  1. 缓存管理:相同请求不会重复发送
  2. 生命周期管理:组件挂载时请求,卸载时清理
  3. Loading 状态:自动追踪 pending 请求数量
  4. 乐观更新:支持 optimisticUpdate 模式
  5. Polling:定时轮询
  6. Pagination:内置分页支持

缓存控制

// 强制重新获取
const { data } = useGetUserQuery(userId, { refetchOnMountOrArgChange: true });

// 当 window 重新获得焦点时重新获取
const { data } = useGetUserQuery(userId, { refetchOnFocus: true });

// 手动触发
dispatch(api.util.invalidateTags(['User']));

RTK 的 Immer 集成

不可变更新的陷阱

RTK 内部使用 Immer,允许你在 reducer 中「修改」state。但这不意味着你可以做任何操作:

// 错误:不要返回新 state,而是修改参数
reducers: {
  increment: (state) => {
    return { ...state, value: state.value + 1 }; // 不要这样做!
  }
}

// 正确:直接修改 state
reducers: {
  increment: (state) => {
    state.value += 1;
  }
}

处理嵌套数据

const todosSlice = createSlice({
  name: 'todos',
  initialState: { todos: [] },
  reducers: {
    updateTodo: (state, action) => {
      const { id, text } = action.payload;
      const todo = state.todos.find(t => t.id === id);
      if (todo) {
        todo.text = text;
      }
    },
    addComment: (state, action) => {
      const { todoId, comment } = action.payload;
      const todo = state.todos.find(t => t.id === todoId);
      if (todo) {
        todo.comments.push(comment);
      }
    }
  }
});

面试中的表达

面试中聊到 Redux Toolkit,通常是在考察你对状态管理的理解深度:

Redux Toolkit 的核心改进是简化了 Redux 的 boilerplate。在传统 Redux 中,你需要写 action types、action creators、reducers、store 配置,每一步都很多模板代码。RTK 通过 createSlice 把这些合并成一个配置对象,reducers 里的代码看起来像直接修改 state,但 Immer 会自动处理不可变性。

createAsyncThunk 是处理异步逻辑的标准方案。它接收一个 async 函数,自动生成 pending/fulfilled/rejected 三种 action type,你只需要在 extraReducers 里处理它们就行。

RTK Query 是专门为 React 数据获取设计的。它不只是状态管理,还包含缓存、生命周期管理、loading 状态。对于需要从 API 获取数据的场景,RTK Query 可以大大减少代码量。


延展阅读