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

Redux Toolkit中Slice间状态异常干扰问题求助

Redux Toolkit多Slice状态污染问题

我用Redux Toolkit开发,项目里有多个独立维护状态的Slice(roles、jobs、users等)。触发getAllRoles异步Action时,roles Slice的loading状态能正确设为true,但jobs、users等其他Slice的loading也被错误设为true,且无法恢复为false;触发getJobs Action时,users Slice的loading同样会被异常修改。Redux DevTools显示roles/getAllRoles/pending Action会修改其他Slice的loading状态。试过移除addMatcher、给Action和Slice设唯一名称、拆分builder.addCase链式调用,问题仍未解决。

roles Slice代码

import { createSlice } from '@reduxjs/toolkit';
import { getAllRoles } from './roleActions';

const initialState = {
  roles: [],
  loading: false,
  isSuccess: false,
  message: '',
};

const authReducer = createSlice({
  name: 'roles',
  initialState,

  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(getAllRoles.pending, (state) => {
        state.isSuccess = false;
        state.message = '';
        state.loading = true;
      })
      .addCase(getAllRoles.fulfilled, (state, { payload }) => {
        state.roles = payload.data;
        state.message = payload.message;
        state.isSuccess = true;
        state.loading = false;
      })
      .addCase(getAllRoles.rejected, (state, { payload }) => {
        state.isSuccess = false;
        state.message = payload;
        state.loading = false;
      });
  },
});

export default authReducer.reducer;

getAllRoles Action代码

import { createAsyncThunk } from '@reduxjs/toolkit';
import API from '../../api';
import { formatError } from '@utils/index';

export const getAllRoles = createAsyncThunk('role/getAllRoles', async (_, { rejectWithValue }) => {
  try {
    const { data } = await API.get('/roles');
    return data;
  } catch (error) {
    return rejectWithValue(formatError(error));
  }
});

调用方式

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

jobs Slice代码

import { createSlice } from '@reduxjs/toolkit';
import { deleteJob, getJobs, updateJob, createJob } from './jobActions';

const initialValues = {
  data: {
    jobs: [],
    totalCount: 0,
  },
  loading: false,
  isSuccess: false,
  message: '',
};

const jobsReducer = createSlice({
  name: 'jobs',
  initialState: initialValues,
  reducers: {},
  extraReducers: (builder) => {
    // Get Jobs
    builder
      .addCase(getJobs.fulfilled, (state, { payload }) => {
        return {
          ...state,
          data: payload.data,
          message: payload.message,
          isSuccess: true,
          loading: false,
        };
      })
      .addCase(deleteJob.fulfilled, (state, { payload }) => {
        const updatedJobs = [...state.data.jobs.filter((job) => job._id !== payload)];
        return {
          ...state,
          data: {
            totalCount: state.data.totalCount - 1,
            jobs: updatedJobs,
          },
          message: 'Job deleted successfully',
          isSuccess: true,
          loading: false,
        };
      })
      .addCase(updateJob.fulfilled, (state, { payload }) => {
        const updatedJobs = [...state.data.jobs.filter((job) => job._id !== payload.data._id), payload.data];
        return {
          ...state,
          data: {
            totalCount: state.data.totalCount,
            jobs: updatedJobs,
          },
          message: payload.message,
          isSuccess: true,
          loading: false,
        };
      })
      .addCase(createJob.fulfilled, (state, { payload }) => {
        return {
          ...state,
          data: {
            totalCount: state.data.totalCount + 1,
            jobs: [payload.data, ...state.data.jobs],
          },
          message: payload.message,
          isSuccess: true,
          loading: false,
        };
      })
      .addMatcher(
        (action) => action.type.endsWith('/pending'),
        (state) => ({
          ...state,
          isSuccess: false,
          message: '',
          loading: true,
        }),
      )
      .addMatcher(
        (action) => action.type.endsWith('/rejected'),
        (state, { payload }) => ({
          ...state,
          isSuccess: false,
          message: payload,
          loading: false,
        }),
      );
  },
});

export default jobsReducer.reducer;

users Slice代码

import { createUser, deleteUser, getUsers, updateUser } from './userActions';

const initialState = {
  users: [],
  loading: false,
  isSuccess: false,
  message: '',
};

const usersReducer = createSlice({
  name: 'users',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    // Get Users
    builder.addCase(getUsers.fulfilled, (state, { payload }) => {
      state.loading = false;
      state.message = payload.message;
      state.users = payload.data;
      state.isSuccess = true;
    });

    // Delete Users
    builder.addCase(deleteUser.fulfilled, (state, { payload }) => {
      state.loading = false;
      state.message = payload.message;
      state.users = [...state.users.filter((user) => user._id !== payload)];
      state.isSuccess = true;
    });

    // Update User
    builder.addCase(updateUser.fulfilled, (state, { payload }) => {
      state.loading = false;
      state.message = payload.message;
      state.users.splice(
        state.users.findIndex((user) => user.email === payload.data.email),
        1,
        payload.data,
      );
      state.isSuccess = true;
      state.message = payload.message;
    });

    // Create User
    builder.addCase(createUser.fulfilled, (state, { payload }) => {
      state.loading = false;
      state.message = payload.message;
      state.users.unshift(payload.data);
      state.isSuccess = true;
    });

    // Pending State
    builder.addMatcher(
      (action) => action.type.endsWith('/pending'),
      (state) => {
        state.isSuccess = false;
        state.message = '';
        state.loading = true;
      },
    );

    // Rejected State
    builder.addMatcher(
      (action) => action.type.endsWith('/rejected'),
      (state, { payload }) => {
        state.isSuccess = false;
        state.message = payload;
        state.loading = false;
      },
    );
  },
});

export default usersReducer.reducer;

Store配置代码

import storage from 'redux-persist/lib/storage';
import { combineReducers } from 'redux';
import { persistReducer } from 'redux-persist';
import thunk from 'redux-thunk';
import { authReducer, usersReducer, rolesReducer, jobsReducer, surveysReducer, pageInfoReducer } from './features';

const reducers = combineReducers({
  auth: authReducer,
  users: usersReducer,
  roles: rolesReducer,
  jobs: jobsReducer,
  survey: surveysReducer,
  pageInfo: pageInfoReducer,
});

const store = configureStore({
  reducer: reducers,
  devTools: process.env.NODE_ENV !== 'production',
  middleware: [thunk],
});

export default store;

问题根源与修复方案

问题原因

jobs和users Slice里的addMatcher使用了action.type.endsWith('/pending')和action.type.endsWith('/rejected')的匹配规则,这会匹配所有异步Action的pending/rejected状态,不管这个Action属于哪个Slice。比如roles/getAllRoles/pending会触发jobs和users Slice里的pending matcher,导致它们的loading被错误设置为true。而且这两个Slice只处理了对应Action的fulfilled状态,没有处理pending和rejected,所以当其他Slice的Action完成时,它们的loading无法恢复。

修复步骤

  1. 为每个Slice的matcher添加前缀过滤:只匹配当前Slice所属的Action。比如jobs Slice只匹配以jobs/开头的Action,users Slice只匹配以users/开头的Action。
  2. 确保Action的type前缀与Slice名称一致:createAsyncThunk的第一个参数要以对应Slice的名称开头,避免匹配混乱。

修改后的jobs Slice关键代码

// 替换原来的addMatcher
.addMatcher(
  (action) => action.type.startsWith('jobs/') && action.type.endsWith('/pending'),
  (state) => ({
    ...state,
    isSuccess: false,
    message: '',
    loading: true,
  }),
)
.addMatcher(
  (action) => action.type.startsWith('jobs/') && action.type.endsWith('/rejected'),
  (state, { payload }) => ({
    ...state,
    isSuccess: false,
    message: payload,
    loading: false,
  }),
)

对应jobActions里的Action要修改前缀:

// jobActions示例
export const getJobs = createAsyncThunk('jobs/getJobs', async () => {
  // ...业务逻辑
});

修改后的users Slice关键代码

// 替换原来的addMatcher
.addMatcher(
  (action) => action.type.startsWith('users/') && action.type.endsWith('/pending'),
  (state) => {
    state.isSuccess = false;
    state.message = '';
    state.loading = true;
  },
)
.addMatcher(
  (action) => action.type.startsWith('users/') && action.type.endsWith('/rejected'),
  (state, { payload }) => {
    state.isSuccess = false;
    state.message = payload;
    state.loading = false;
  },
)

对应userActions里的Action要修改前缀:

// userActions示例
export const getUsers = createAsyncThunk('users/getUsers', async () => {
  // ...业务逻辑
});
  1. 可选优化:移除全局matcher,为每个Action单独处理pending/rejected
    如果不想用matcher,也可以为每个异步Action单独添加pending和rejected的处理,这样更明确,避免匹配错误:
// users Slice示例
builder
  .addCase(getUsers.pending, (state) => {
    state.isSuccess = false;
    state.message = '';
    state.loading = true;
  })
  .addCase(getUsers.rejected, (state, { payload }) => {
    state.isSuccess = false;
    state.message = payload;
    state.loading = false;
  })
  // 其他Action的pending/rejected同理

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 05:34:53