You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Redux Toolkit中正确处理CRUD操作的加载状态?

在Redux Toolkit中处理CRUD加载状态的最佳实践

你的困扰其实是很多Redux开发者都会遇到的典型问题:既要避免全局状态互相干扰,又要保持代码的可维护性。结合Redux Toolkit的设计理念,我推荐用结构化的方式为每个操作独立维护状态,这是平衡简洁性和实用性的最优解。

核心思路

放弃单一的status或loading变量,转而在state中创建一个statuses对象(或者loadingStates),用操作名称作为键,存储该操作的当前状态(推荐用idle/loading/succeeded/failed这几个标准值,这也是Redux官方文档推荐的状态枚举)。这样每个CRUD操作的状态完全隔离,不会互相干扰,同时结构清晰,维护成本低。

同时,错误状态也可以用类似的errors对象来存储,对应每个操作的错误信息。

修改后的Slice实现

首先更新你的initialState,添加结构化的状态字段:

const initialState = {
  posts: [],
  selectedPost: {},
  // 为每个操作维护独立状态
  statuses: {
    createPost: 'idle',
    getPosts: 'idle',
    deletePost: 'idle',
    getPostById: 'idle'
  },
  // 对应每个操作的错误信息
  errors: {
    createPost: null,
    getPosts: null,
    deletePost: null,
    getPostById: null
  }
};

然后用Redux Toolkit推荐的builder语法重构extraReducers(比对象字面量更灵活,尤其是处理异步action时):

export const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    // 创建帖子
    builder
      .addCase(createPost.pending, (state) => {
        state.statuses.createPost = 'loading';
        state.errors.createPost = null;
      })
      .addCase(createPost.fulfilled, (state) => {
        state.statuses.createPost = 'succeeded';
      })
      .addCase(createPost.rejected, (state, action) => {
        state.statuses.createPost = 'failed';
        state.errors.createPost = action.error.message;
      })
      // 获取所有帖子
      .addCase(getPosts.pending, (state) => {
        state.statuses.getPosts = 'loading';
        state.errors.getPosts = null;
      })
      .addCase(getPosts.fulfilled, (state, action) => {
        state.statuses.getPosts = 'succeeded';
        state.posts = action.payload.data;
      })
      .addCase(getPosts.rejected, (state, action) => {
        state.statuses.getPosts = 'failed';
        state.errors.getPosts = action.error.message;
      })
      // 删除帖子
      .addCase(deletePost.pending, (state) => {
        state.statuses.deletePost = 'loading';
        state.errors.deletePost = null;
      })
      .addCase(deletePost.fulfilled, (state, action) => {
        state.statuses.deletePost = 'succeeded';
        state.posts = state.posts.filter(el => el.id !== action.payload);
      })
      .addCase(deletePost.rejected, (state, action) => {
        state.statuses.deletePost = 'failed';
        state.errors.deletePost = action.error.message;
      })
      // 根据ID获取帖子
      .addCase(getPostById.pending, (state) => {
        state.statuses.getPostById = 'loading';
        state.errors.getPostById = null;
      })
      .addCase(getPostById.fulfilled, (state, action) => {
        state.statuses.getPostById = 'succeeded';
        state.selectedPost = action.payload;
      })
      .addCase(getPostById.rejected, (state, action) => {
        state.statuses.getPostById = 'failed';
        state.errors.getPostById = action.error.message;
      });
  }
});

组件中的使用方式

在组件里,你可以精准地判断某个操作的状态,比如:

// 获取所有帖子的加载状态
const getPostsLoading = useSelector(state => state.posts.statuses.getPosts === 'loading');
// 创建帖子的加载状态
const createPostLoading = useSelector(state => state.posts.statuses.createPost === 'loading');

// 在渲染时对应显示加载器
if (getPostsLoading) {
  return <PostsListLoader />;
}

// 创建按钮的加载状态
<button disabled={createPostLoading}>
  {createPostLoading ? '创建中...' : '创建帖子'}
</button>

对你尝试过的方案的补充说明

  1. 每个操作单独加状态变量:这种方式可行但不够优雅,变量会零散分布,而用statuses对象可以把相关状态聚合在一起,更符合Redux的状态结构化原则。
  2. 带标识的状态字符串:确实会增加记忆成本,且容易拼写错误,结构化的对象模式更直观,IDE还能提供自动补全。
  3. 统一loading/error状态:正如你发现的,多个操作会互相覆盖,完全不可行,除非你的应用只有一个异步操作在同一时间执行,这显然不符合CRUD场景。
  4. 拆分独立slice:完全没必要,CRUD操作都属于posts领域的状态,放在同一个slice里更符合单一职责原则,拆分反而会增加状态管理的复杂度。

进阶方案:使用RTK Query

如果你的CRUD操作都是和API交互,那么Redux Toolkit内置的RTK Query会是更省心的选择——它会自动为每个请求维护isLoading、isSuccess、isError等状态,你完全不需要手动编写extraReducers。比如定义一个API slice:

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

export const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => 'posts',
    }),
    getPostById: builder.query({
      query: (id) => `posts/${id}`,
    }),
    createPost: builder.mutation({
      query: (postData) => ({
        url: 'posts',
        method: 'POST',
        body: postData,
      }),
    }),
    deletePost: builder.mutation({
      query: (id) => ({
        url: `posts/${id}`,
        method: 'DELETE',
      }),
    }),
  }),
});

// 自动生成的hooks,直接在组件中使用
export const { useGetPostsQuery, useGetPostByIdQuery, useCreatePostMutation, useDeletePostMutation } = postsApi;

在组件中使用时,直接解构获取状态:

const { data: posts, isLoading: getPostsLoading } = useGetPostsQuery();
const [createPost, { isLoading: createPostLoading }] = useCreatePostMutation();

RTK Query还自带缓存、自动重取等功能,非常适合API驱动的应用。

内容的提问来源于stack exchange,提问作者Rostyk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 22:02:30