如何为单个 属性 嵌套 Redux Toolkit reducer

How to nest Redux Toolkit reducers for a single property

我正在将代码库从 vanilla Redux 迁移到 Redux Toolkit。我正在尝试找到一种很好的方法来嵌套使用 createReducer 创建的 reducer 仅用于单个 属性.

假设我有一个类似于以下人为示例的设置,其中包含一个 user reducer 和一个嵌套在其下的 friends reducer。用户可以更改他们的名字,这只会影响他自己,也可以添加和删除他们的朋友,这会影响他自己和它的 friends 数组 属性,它由 friends reducer 管理。

const CHANGE_NAME = "CHANGE_NAME";
const ADD_FRIEND = "ADD_FRIEND";
const REMOVE_ALL_FRIENDS = "REMOVE_ALL_FRIENDS";

const initialState = {
  username: "",
  email: "",
  lastActivity: 0,
  friends: [],
};

const user = (state = initialState, action) => {
  switch (action.type) {
    case CHANGE_NAME: {
      const { newName, time } = action.payload;

      return {
        ...state,
        name: newName,
        lastActivity: time,
      };
    }
    case ADD_FRIEND:
    case REMOVE_ALL_FRIENDS: {
      const { time } = action.payload;

      return {
        ...state,
        friends: friends(state.friends, action),
        lastActivity: time,
      };
    }
    default: {
      return {
        ...state,
        friends: friends(state.friends, action),
      };
    }
  }
};

const friends = (state = initialState.friends, action) => {
  switch (action.type) {
    case ADD_FRIEND: {
      const { newFriend } = action.payload;

      return [...state, newFriend];
    }
    case REMOVE_ALL_FRIENDS: {
      return [];
    }
    default: {
      return state;
    }
  }
};

注意事项:

我现在正尝试使用 Redux Toolkit createReducers 重构它。我的第一次尝试如下:

import { createReducer } from "@reduxjs/toolkit";

const CHANGE_NAME = "CHANGE_NAME";
const ADD_FRIEND = "ADD_FRIEND";
const REMOVE_ALL_FRIENDS = "REMOVE_ALL_FRIENDS";

const initialState = {
  username: "",
  email: "",
  lastActivity: 0,
  friends: [],
};

const user = createReducer(initialState, (builder) => {
  builder
    .addCase(CHANGE_NAME, (state, action) => {
      const { newName, time } = action.payload;

      state.name = newName;
      state.lastActivity = time;
    })
    .addMatcher((action) => action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS),
    (state, action) => {
      const { time } = action.payload;
      state.lastActivity = time;
      state.friends = friends(state.friends, action);
    };
});

const friends = createReducer(initialState, (builder) => {
  builder
    .addCase(ADD_FRIEND, (state, action) => {
      const { newFriend } = action.payload;

      state.push(newFriend);
    })
    .addCase(REMOVE_ALL_FRIENDS, () => []);
});

注意事项:

根据我的直觉,这会起作用,因为它看起来是相同的逻辑。但是,这仅适用于 ADD_FRIEND 操作,并且不执行任何操作或发出有关同时修改状态和 return 为 REMOVE_ALL_FRIENDS 操作类型创建新状态的错误。

这似乎是因为被修改的状态将它变成 user reducer 中的 ImmerJS Proxy 对象,但是当它被传递到 friends reducer 并且它 return 是一个状态对象,而不是直接修改它导致 RTK 抛出错误,因为它说您只能修改 return 状态,但不能同时修改两者。在 ADD_FRIEND 的处理程序中,这不是问题,因为它总是修改状态,与 user.

中的所有处理程序一样

作为一个 hacky 解决方法,我手动检查了 friends reducer return 是一个 Proxy 还是一个新状态,如果它 return 是一个新状态,那么它在 user reducer 中设置它,但我相信有更好的方法:

import { createReducer, current } from "@reduxjs/toolkit";

const user = createReducer(initialState, (builder) => {
  builder
    .addMatcher((action) => action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS),
    (state, action) => {
      const { time } = action.payload;

      state.lastActivity = time;

      const result = friends(state.friends, action);

      let output;
      
      // If state has been returned directly this will error and we can set the state manually,
      // Else this will not error because a Proxy has been returned, and thus the state has been
      // set already by the sub-reducer.
      try {
        output = current(result);
      } catch (error) {
        output = result;
      }

      if (output) {
        state.progress = output;
      }
    };
});

我的问题是如何解决这个问题,这样我就不必手动检查 return 类型,并且可以轻松地将 RTK reducers 嵌套在彼此之间,无论是通过重组我的 reducers 还是修复代码逻辑?

理想情况下,我仍然希望将 friends reducer 嵌套在 user reducer 下,因为很多“vanilla”Redux 代码都是这样构造它们的状态逻辑的,许多不同的 reducer 处理许多不同的状态,而不是通过单个 combineReducers 调用将它们全部嵌套在根级别,但如果有更好更清洁的解决方案,我也很好。

感谢您的帮助,很抱歉 post - 只是想尽可能详细,因为其他在线解决方案似乎没有解决这个确切的问题。

这实际上可能是 Redux Toolkit 中的错误。您能否在 github 问题跟踪器上提交一个复制 CodeSandbox 的问题?

在这种特殊情况下,很容易处理 user reducer 中的 friends 属性:state.friends.push(newFriend)state.friends = []。但是将它分开应该没有任何问题。

我在尝试 运行 您的代码时确实注意到了一些问题:

  • 使用整个用户的initialState作为friends的初始状态,而不是initialState.friends[]
  • addMatcher 中围绕 action.type 检查
  • 的括号不匹配
  • 分配给 state.name 而不是 state.username

修复这些问题后,我无法重现您的问题。我可以成功添加和删除朋友。

问题是我原来的 user reducer 代码是 reducer 通过传播状态并在该对象传播中设置 friends 属性 来返回一个新的状态对象。这从 ImmerJS 中产生了一个错误,因为它从 user reducer 返回一个新值,同时也在 friends reducer 中修改它。

我发布的代码有效(由于 Linda 做了一些修改),但为了修复我的原始代码(我没有发布产生错误的版本 - 抱歉)我必须更改以下内容:

.addMatcher(
  (action) =>
    action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS,
  (state, action) => ({
    ...state,
    lastActivity: action.payload.time,
    friends: friends(state.friends, action)
  })
)

至:

.addMatcher(
  (action) =>
    action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS,
  (state, action) => {
    const { time } = action.payload;
    state.lastActivity = time;
    state.friends = friends(state.friends, action);
  }
)

谢谢大家的帮助。