在 "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>'
尝试解决问题
- 使用扩展运算符在 reducer 中创建
initialState
的新副本。
const copyInitialState = { ...initialState }
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
- 使用 Object.assign() 在 reducer 中创建一个新对象
const copyInitialState = {};
Object.assign(copyInitialState, initialState);
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
- 在调用
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;
},
}
});
上下文
我正在尝试为多级手风琴菜单创建状态,其中顶级项目称为主题,每个主题有多个章节,每个章节将有多篇文章。
在任何给定时间,只有一个“主题”可以处于“已选择”状态。这同样适用于章节和文章,额外的限制是它们必须是处于“选定”状态的父项的“子项”。
代码
我有一个深层嵌套的对象,它作为初始状态传递给 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>'
尝试解决问题
- 使用扩展运算符在 reducer 中创建
initialState
的新副本。
const copyInitialState = { ...initialState }
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
- 使用 Object.assign() 在 reducer 中创建一个新对象
const copyInitialState = {};
Object.assign(copyInitialState, initialState);
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
- 在调用
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;
},
}
});