在 "Redux Toolkits" createSlice() 中传递的 reducer 函数中改变初始状态的 "copy" 是一种不好的做法吗?

Is mutating "copy" of initial state inside a passed reducer function in "Redux Toolkits" createSlice() a bad practice?

上下文

我正在尝试为多级手风琴菜单创建状态,其中顶级项目称为主题,每个主题有多个章节,每个章节将有多篇文章。

在任何给定时间,只有一个“主题”可以处于“已选择”状态。这同样适用于章节和文章,额外的限制是它们必须是处于“选定”状态的父项的“子项”。

代码

我有一个深层嵌套的对象,它作为初始状态传递给 createSlice() 方法,它具有以下形状,

const initialState = {
  currentArticle: null,
  currentChapter: null,
  currentSubject: null
  subjects: [
    {
     id:"001",
     chapters: [
        {
         id: "001001",
         articles: [
          {
           id: "001001001",
           selected: false
          },
          //....... more articles
         ],
         selected: false
       },
       //....... more chapters
     ],
     selected: false
    },
    //....... more subjects
  ]
}

以下是我的createSlice()方法,

export const articleNavigationSlice = createSlice({
  name: "articlenav",
  initialState: initialState,
  reducers: {
    setTopic: (state, { payload }) => {
      const newNavigationState = initialState.subjects.map((subject) => {
        if (payload.id === subject.id) {
          subject.selected = true;
          state.currentSubject = subject.id
        } 
        return subject;
      });
      state.subjects = newNavigationState;
    },
    // ...... more reducer functions
  },
});

subjects数组直接用于渲染UI,每次调用dispatch函数我有条件的使用初始状态,然后计算下一个状态,(为简单起见,这些条件未包含在以下代码片段中),现在让我们考虑每次我需要计算下一个状态时使用初始状态“subject”数组使用传递给减速器的先前状态。

使用初始状态的原因是不必手动将嵌套对象的选定状态设置为 false,以防父级 selected 状态发生变化。

问题

然而,当我分派执行“setTopic”reducer 函数的操作时,出现以下错误,

TypeError: Cannot assign to read only property 'selected' of object '#<Object>'

尝试解决问题

  1. 使用扩展运算符在 reducer 中创建 initialState 的新副本。
const copyInitialState = { ...initialState }
const newNavigationState =  copyInitialState.subjects.map((subject) => {
//............
  1. 使用 Object.assign() 在 reducer 中创建一个新对象
const copyInitialState = {};
Object.assign(copyInitialState, initialState);
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
  1. 在调用 createSlice 之前创建初始状态的 2 个副本,并在 createSlice() 调用中传递一个副本作为初始状态,并在传递的 reducer 函数中使用另一个副本。
const initialStateCopy = Object.assign(initialState);
const initializedInitialState = Object.assign(initialState);

export const articleNavigationSlice = createSlice({
  name: "articlenav",
  initialState: initializedInitialState,
  reducers: {
    setTopic: (state, { payload }) => {
      const newNavigationState = initialStateCopy.subjects.map((subject) => {
//............

I.E:我也尝试过使用展开运算符的这种方法。

唯一可行的解​​决方案(不是理想的方法)

显式声明一个全新的常量并以与 initialState 对象完全相同的方式初始化它。在这种情况下,这只是意味着我正在一个接一个地复制完全相同的对象创建代码,以便它们完全是两个不同的对象,

const initialState = {//.... deeply nested object}
const initialStateExplicitCopy = {//.... deeply nested object}

export const articleNavigationSlice = createSlice({
  name: "articlenav",
  initialState: initialState,
  reducers: {
    setTopic: (state, { payload }) => {
      const newNavigationState = initialStateExplicitCopy.subjects.map((subject) => {

问题

我认为这与 Immer 以及它如何处理初始状态对象有关。我看到即使我做了 Object.assign() 嵌套对象也是密封和冻结的。

这是否意味着我试图执行错误的操作?或者什么被认为是不好的做法?这会以任何方式使减速器不纯吗?如果是这样我不明白为什么因为初始状态永远不会改变,我只是一直使用初始状态来计算下一个状态。

在使用 redux 工具包时有没有更好的方法来处理这个问题?

是的,问题是出于几个不同的原因试图变异 initialState

如果数据实际上已经通过 Immer 传递并包装在代理中,那么编写“变异”数据的代码才是安全的,这样 Immer 就可以跟踪尝试的更改。当您引用 initialState 时,该对象尚未交给 Immer,因此您的代码实际上是 尝试 来改变 initialState.

幸运的是,当您调用 createSlice({initialState: someInitialStateValue}) 时,createSlice 会在内部“冻结”该值,以确保您以后不会意外地真正改变它。这就是您收到错误的原因 - 它告诉您 做错了什么。

从概念上讲,我不确定为什么 你试图总是基于 initialState 进行计算。您不想以 当前 状态为起点进行更新吗?

如果你确实需要使用 initialState 作为起点,最好的选择是直接使用 Immer 并喂它 initialState。 Immer 的主要功能从 RTK 导出为 createNextState,因此您可以使用它来包装您当前的逻辑

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

export const articleNavigationSlice = createSlice({
  name: "articlenav",
  initialState: initialState,
  reducers: {
    setTopic: (state, { payload }) => {
      const newNavigationState = createNextState(initialState.subjects, draftSubjects) => {
        const subject = draftSubjects.find(subject => subject.id === payload.id);
        if (subject) {
          subject.selected = true;
          state.currentSubject = subject.id
        }
      }
      state.subjects = newNavigationState;
    },
  }
});