RTK

191 阅读9分钟

✨ 1 RTK 概述与核心优势

Redux Toolkit(RTK)是 Redux 官方推荐的高效开发工具集,旨在简化 Redux 的配置与开发流程。它通过一组经过深思熟虑且“有意见”的 API,解决了 Redux 学习曲线陡峭、配置复杂以及书写大量样板代码的问题。

1.1 为什么需要 RTK?

传统 Redux 开发中,你通常需要手动配置 store、编写大量的 action types、action creators 和 reducers,并且为了处理异步逻辑,还需要额外添加中间件如 redux-thunk。RTK 通过提供一系列工具,内置了最佳实践,极大地提升了开发体验和代码质量。

1.2 RTK 的核心优势

  • 简化配置configureStore 提供了开箱即用的默认配置,包括集成 Redux DevTools 和默认包含 redux-thunk 中间件。
  • 减少样板代码createSlice 自动生成 action types、action creators 和 reducers,你只需定义 reducer 函数和初始状态。
  • 内置不可变更新逻辑:使用 Immer 库,允许你在 reducers 中编写“可变”逻辑,但实际上生成的是不可变更新。
  • 强大的异步处理createAsyncThunk 简化了异步 action 的处理,RTK Query 则提供了强大的数据获取和缓存能力。
  • 优秀的 TypeScript 支持:RTK 从一开始就为 TypeScript 设计,提供了良好的类型安全。

🛠️ 2 安装与设置

2.1 安装 RTK 和 React-Redux

在你的 React 项目中,使用 npm 或 yarn 安装必要的包:

npm install @reduxjs/toolkit react-redux
# 或
yarn add @reduxjs/toolkit react-redux

2.2 创建 Redux Store

使用 configureStore 来创建 Redux store。它简化了 store 的配置过程,并设置了一些有用的默认值。

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

// 使用 configureStore 创建 store
export default configureStore({
  reducer: {
    counter: counterReducer, // 添加 slice reducer
    // 可以在此添加其他 slice reducers
  },
  // middleware, devTools 等选项均有智能默认值,通常无需额外配置
});

configureStore 会自动:

  • 组合你的 slice reducers
  • 添加 redux-thunk 中间件以支持异步操作
  • 启用 Redux DevTools Extension 以便调试

2.3 为 React 提供 Redux Store

你需要用 <Provider> 组件包裹你的 React 应用,并将上面创建的 store 作为 prop 传递给它,这样 React 组件才能访问到 Redux store。

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    {/* 提供 store */}
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

🧩 3 核心概念与基本用法

3.1 使用 createSlice 创建 State Slice

createSlice 是 RTK 中一个非常重要的 API,它允许你通过定义一个 slice 来生成 action creators 和 reducer,无需手动编写大量的样板代码。

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

// 定义初始状态
const initialState = {
  value: 0,
};

// 创建一个 slice
export const counterSlice = createSlice({
  name: 'counter', // slice 的名称,用于生成 action types
  initialState,    // 初始状态
  reducers: {      // 定义 reducers
    // increment  reducer
    increment: (state) => {
      // 这里看起来像是直接修改了 state,但由于 Immer 的存在,实际上是安全的不可变更新
      state.value += 1;
    },
    // decrement reducer
    decrement: (state) => {
      state.value -= 1;
    },
    // incrementByAmount reducer,接受一个参数
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

// 自动生成的 action creators
// 每个 reducer 函数都会生成一个对应的 action creator
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// 导出 slice 的 reducer
export default counterSlice.reducer;

关键点说明

  • name:slice 的名称,用于生成 action type 的前缀(例如 counter/increment)。
  • initialState:该 slice 的初始状态值。
  • reducers:一个对象,其中每个键都会生成一个 action creator 函数和一个 reducer case。RTK 使用 Immer 库,允许你以“可变”的方式编写更新逻辑,但最终会生成不可变更新。
  • 生成的 Action CreatorscreateSlice 会自动为每个 reducer 函数生成一个对应的 action creator(例如 increment() 返回 { type: 'counter/increment' })。
  • Slice Reducer:需要将其导出并添加到 store 的 reducer 配置中。

3.2 在 React 组件中使用 State 和 Actions

在 React 组件中,你可以使用 react-redux 提供的 useSelectoruseDispatch 钩子来与 Redux store 交互。

// features/counter/Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { decrement, increment, incrementByAmount } from './counterSlice';
import styles from './Counter.module.css';

export function Counter() {
  // useSelector 从 store 的 state 中提取数据
  // 这里获取的是 state.counter.value(因为我们在 configureStore 中将 counterReducer 放在了 `counter` 键下)
  const count = useSelector((state) => state.counter.value);
  
  // useDispatch 返回 store 的 dispatch 方法,用于分发 actions
  const dispatch = useDispatch();

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())} // 分发 increment action
        >
          Increment
        </button>
        <span>{count}</span> {/* 显示计数器的值 */}
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())} // 分发 decrement action
        >
          Decrement
        </button>
        <button
          onClick={() => dispatch(incrementByAmount(5))} // 分发带有 payload 的 action
        >
          Increment by 5
        </button>
      </div>
    </div>
  );
}

关键点说明

  • useSelector:这是一个钩子,允许你从 Redux store 状态树中提取所需的任何数据。它接受一个选择器函数,其参数是整个 store state,返回的是你需要的部分状态。
  • useDispatch:这个钩子返回一个对 store 的 dispatch 方法的引用,你可以用它来分发 actions。
  • 分发 Actions:通过调用 dispatch(actionCreator(...args)) 来改变 store 中的状态。组件通常会在用户交互(如点击按钮)时分发 actions。

⚡ 4 异步逻辑与数据获取

处理异步操作(如 API 调用)是前端应用中的常见需求。RTK 提供了 createAsyncThunkRTK Query 来简化这些操作。

4.1 使用 createAsyncThunk 处理异步 Actions

createAsyncThunk 用于创建异步 thunk action creator,它会根据给定的 action type 和一个返回 Promise 的函数,自动生成 pending, fulfilled, rejected 三种 action types,并分发相应的 actions。

// features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

// 创建一个异步 thunk 来获取帖子列表
export const fetchPosts = createAsyncThunk(
  'posts/fetchPosts', // action type 前缀
  async () => {
    const response = await axios.get('https://api.example.com/posts');
    return response.data; // 此值将作为 action payload 传递给 fulfilled action
  }
);

const postsSlice = createSlice({
  name: 'posts',
  initialState: {
    items: [],
    loading: 'idle', // 或 'pending', 'succeeded', 'failed'
    error: null
  },
  reducers: {
    // 同步 reducers (可选)
  },
  // 使用 extraReducers 来处理由 createAsyncThunk 或其他其他地方定义的 actions
  extraReducers: (builder) => {
    builder
      .addCase(fetchPosts.pending, (state) => {
        state.loading = 'pending';
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.loading = 'succeeded';
        state.items = action.payload; // 将获取到的数据存入 state
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.loading = 'failed';
        state.error = action.error.message;
      });
  }
});

export default postsSlice.reducer;

在组件中使用异步 thunk:

// features/posts/PostsList.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPosts } from './postsSlice';

export function PostsList() {
  const dispatch = useDispatch();
  const posts = useSelector((state) => state.posts.items);
  const postStatus = useSelector((state) => state.posts.loading);
  const error = useSelector((state) => state.posts.error);

  useEffect(() => {
    if (postStatus === 'idle') {
      dispatch(fetchPosts()); // 组件挂载后获取帖子列表
    }
  }, [postStatus, dispatch]);

  let content;

  if (postStatus === 'pending') {
    content = <div>Loading...</div>;
  } else if (postStatus === 'succeeded') {
    content = posts.map(post => (
      <div key={post.id}>{post.title}</div>
    ));
  } else if (postStatus === 'failed') {
    content = <div>{error}</div>;
  }

  return (
    <section>
      <h2>Posts</h2>
      {content}
    </section>
  );
}

4.2 使用 RTK Query 进行数据获取和缓存

RTK Query 是 RTK 中包含的一个强大的数据获取和缓存工具。它专为 Redux 设计,可以简化网络请求、缓存管理、加载状态显示等常见需求,无需编写大量的 thunks 和 reducers

// api/apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

// 使用 createApi 创建 API slice
export const apiSlice = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }), // 基础请求 URL
  tagTypes: ['Post'], // 定义标签类型用于缓存失效和重新获取
  endpoints: (builder) => ({
    // 定义查询端点
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: ['Post'], // 此查询提供 'Post' 标签
    }),
    // 定义变更端点(如创建、更新、删除)
    addNewPost: builder.mutation({
      query: (initialPost) => ({
        url: '/posts',
        method: 'POST',
        body: initialPost,
      }),
      invalidatesTags: ['Post'], // 此变更使 'Post' 标签失效,触发相关查询重新获取
    }),
  }),
});

// 自动生成的 React hooks
export const { useGetPostsQuery, useAddNewPostMutation } = apiSlice;

在组件中使用 RTK Query:

// features/posts/PostsListWithRTKQ.js
import React from 'react';
import { useGetPostsQuery, useAddNewPostMutation } from '../api/apiSlice';

export function PostsListWithRTKQ() {
  // 使用自动生成的 hook 进行查询
  const {
    data: posts,
    isLoading,
    isSuccess,
    isError,
    error
  } = useGetPostsQuery(); // 缓存、加载状态等均由 RTK Query 自动管理

  // 使用自动生成的 hook 进行变更
  const [addNewPost, { isLoading: isAdding }] = useAddNewPostMutation();

  let content;

  if (isLoading) {
    content = <div>Loading...</div>;
  } else if (isSuccess) {
    content = posts.map(post => (
      <div key={post.id}>{post.title}</div>
    ));
  } else if (isError) {
    content = <div>{error.toString()}</div>;
  }

  return (
    <section>
      <h2>Posts (with RTK Query)</h2>
      <button onClick={() => addNewPost({ title: 'New Post', content: '...' })}>
        Add Post
      </button>
      {content}
    </section>
  );
}

RTK Query 的优势

  • 自动化缓存:自动缓存数据,避免重复请求。
  • 生命周期管理:自动管理加载状态、错误状态。
  • 数据失效和重新获取:通过标签系统轻松实现数据同步。
  • 优化性能:内置了对重复请求的去重和查询结果的优化选择。
  • 减少代码量:无需手动编写异步 thunks、reducers 来处理加载状态和结果。

🧠 5 最佳实践与项目结构

5.1 项目结构建议

一个良好的项目结构有助于代码维护和团队协作。对于使用 RTK 的 React 项目,可以考虑按功能(feature)组织:

src/
├── app/
│   ├── store.js          # Store 配置
│   └── hooks.js          # 可选的,用于导出类型化的 hooks(如果使用 TypeScript)
├── features/
│   ├── counter/          # Counter 功能模块
│   │   ├── counterSlice.js
│   │   └── Counter.js
│   └── posts/            # Posts 功能模块
│       ├── postsSlice.js
│       └── PostsList.js
├── api/                  # RTK Query API slice(s)
│   └── apiSlice.js
└── index.js

5.2 性能优化

  • 记忆化 Selectors:使用 reselect 库的 createSelector 创建记忆化的 selectors,避免不必要的重计算和组件重渲染。RTK 从 @reduxjs/toolkit 导出了 createSelector
  • 规范化状态:对于嵌套或关系型数据,考虑使用规范化结构来避免重复和数据不一致。RTK 提供了 createEntityAdapter 来帮助管理规范化数据。

5.3 调试

  • Redux DevTools:RTK 的 configureStore 默认启用了 Redux DevTools Extension。你可以利用它来查看 action 日志、状态快照、进行“时间旅行”调试等。

📊 传统 Redux 与 RTK 写法对比

下面的表格对比了传统 Redux 和 RTK 在一些常见操作上的写法差异,帮助你更直观地理解 RTK 带来的简化:

功能传统 ReduxRedux Toolkit (RTK)RTK 的优势说明
创建 Store手动调用 createStore,需自行组合 reducers,手动添加中间件(如 thunk、DevTools)。使用 configureStore,自动组合 reducers,默认包含 thunk 和 DevTools。开箱即用,减少样板代码,默认包含常用工具。
定义 Actions需定义 action types 常量,并编写 action creator 函数。使用 createSlice自动生成 action types 和 action creators。极大减少样板代码,避免手动编写和维护 action 相关代码。
编写 Reducer通常使用 switch 语句,需手动处理不可变更新,逻辑复杂易出错。使用 createSlicereducers支持"可变"语法(Immer 支持),自动生成不可变更新。简化 reducer 逻辑,更直观的代码,减少不可变更新错误。
异步操作需自行安装和配置中间件(如 redux-thunk),手动分发 pending/fulfilled/rejected 等 actions。使用 createAsyncThunk自动生成处理异步生命周期的 actions;或使用 RTK Query内置异步处理,自动化异步状态管理;RTK Query 极大简化数据获取和缓存
TypeScript需大量手动定义类型。一流的 TS 支持,API 设计充分考虑类型推断,减少类型定义工作。更好的开发体验和类型安全。

💡 提示:RTK 的核心在于简化约定。它提供了开箱即用的 sensible defaults(合理的默认值)和强大的工具,让你能更高效地编写 Redux 代码。建议你遵循其官方推荐的模式和最佳实践。

💎 总结

Redux Toolkit 通过提供一系列强大的 API(如 configureStore, createSlice, createAsyncThunk, RTK Query),极大地改善了 Redux 的开发体验。它减少了样板代码,内置了最佳实践(如 Immer 用于不可变更新、默认包含常用中间件),并提供了出色的 TypeScript 支持。