如何在 redux 中更新特定数组项内的单个值
How to update single value inside specific array item in redux
我遇到了一个问题,即重新呈现状态会导致 ui 问题,并且建议只更新我的 reducer 中的特定值以减少页面上的重新呈现量。
这是我所在州的例子
{
name: "some name",
subtitle: "some subtitle",
contents: [
{title: "some title", text: "some text"},
{title: "some other title", text: "some other text"}
]
}
我目前正在这样更新它
case 'SOME_ACTION':
return { ...state, contents: action.payload }
其中 action.payload
是包含新值的整个数组。但现在我实际上只需要更新内容数组中第二项的文本,而这样的事情不起作用
case 'SOME_ACTION':
return { ...state, contents[1].text: action.payload }
其中 action.payload
现在是我需要更新的文本。
您不必在一行中完成所有操作:
case 'SOME_ACTION': {
const newState = { ...state };
newState.contents =
[
newState.contents[0],
{title: newState.contents[1].title, text: action.payload}
];
return newState
};
您可以使用 React Immutability helpers
import update from 'react-addons-update';
// ...
case 'SOME_ACTION':
return update(state, {
contents: {
1: {
text: {$set: action.payload}
}
}
});
虽然我想你可能会做更多类似的事情?
case 'SOME_ACTION':
return update(state, {
contents: {
[action.id]: {
text: {$set: action.payload}
}
}
});
您可以使用 map
。这是一个示例实现:
case 'SOME_ACTION':
return {
...state,
contents: state.contents.map(
(content, i) => i === 1 ? {...content, text: action.payload}
: content
)
}
派对已经很晚了,但这里有一个适用于每个索引值的通用解决方案。
您创建一个新数组并将其从旧数组扩展到您要更改的 index
。
添加你想要的数据。
创建一个新数组并将其展开,从您要更改的 index
到数组末尾
let index=1;// probably action.payload.id
case 'SOME_ACTION':
return {
...state,
contents: [
...state.contents.slice(0,index),
{title: "some other title", text: "some other text"},
...state.contents.slice(index+1)
]
}
更新:
我做了一个小模块来简化代码,所以你只需要调用一个函数:
case 'SOME_ACTION':
return {
...state,
contents: insertIntoArray(state.contents,index, {title: "some title", text: "some text"})
}
有关更多示例,请查看 repository
函数签名:
insertIntoArray(originalArray,insertionIndex,newData)
编辑:
还有Immer.js库可以处理各种值,它们也可以深度嵌套。
我相信当你需要在你的 Redux 状态上进行这种操作时 传播运算符是你的朋友并且这个原则适用于所有 children.
让我们假装这是你的状态:
const state = {
houses: {
gryffindor: {
points: 15
},
ravenclaw: {
points: 18
},
hufflepuff: {
points: 7
},
slytherin: {
points: 5
}
}
}
你想给拉文克劳加3分
const key = "ravenclaw";
return {
...state, // copy state
houses: {
...state.houses, // copy houses
[key]: { // update one specific house (using Computed Property syntax)
...state.houses[key], // copy that specific house's properties
points: state.houses[key].points + 3 // update its `points` property
}
}
}
通过使用扩展运算符,您可以只更新新状态,而其他一切都保持不变。
示例取自此 amazing article,您可以找到几乎所有可能的选项以及很好的示例。
在我的例子中,我根据路易斯的回答做了这样的事情:
// ...State object...
userInfo = {
name: '...',
...
}
// ...Reducer's code...
case CHANGED_INFO:
return {
...state,
userInfo: {
...state.userInfo,
// I'm sending the arguments like this: changeInfo({ id: e.target.id, value: e.target.value }) and use them as below in reducer!
[action.data.id]: action.data.value,
},
};
这就是我为我的一个项目所做的:
const markdownSaveActionCreator = (newMarkdownLocation, newMarkdownToSave) => ({
type: MARKDOWN_SAVE,
saveLocation: newMarkdownLocation,
savedMarkdownInLocation: newMarkdownToSave
});
const markdownSaveReducer = (state = MARKDOWN_SAVED_ARRAY_DEFAULT, action) => {
let objTemp = {
saveLocation: action.saveLocation,
savedMarkdownInLocation: action.savedMarkdownInLocation
};
switch(action.type) {
case MARKDOWN_SAVE:
return(
state.map(i => {
if (i.saveLocation === objTemp.saveLocation) {
return Object.assign({}, i, objTemp);
}
return i;
})
);
default:
return state;
}
};
恐怕使用数组的 map()
方法可能会很昂贵,因为要迭代整个数组。相反,我组合了一个由三部分组成的新数组:
- head - 修改项
之前的项
- 修改项
- tail - 修改项
之后的项
这里是我在代码中使用的示例(NgRx,但其他 Redux 实现的机制相同):
// toggle done property: true to false, or false to true
function (state, action) {
const todos = state.todos;
const todoIdx = todos.findIndex(t => t.id === action.id);
const todoObj = todos[todoIdx];
const newTodoObj = { ...todoObj, done: !todoObj.done };
const head = todos.slice(0, todoIdx - 1);
const tail = todos.slice(todoIdx + 1);
const newTodos = [...head, newTodoObj, ...tail];
}
这在 redux-toolkit 中非常简单,它使用 Immer 帮助您编写看起来像可变的不可变代码,更简洁易读。
// it looks like the state is mutated, but under the hood Immer keeps track of
// every changes and create a new state for you
state.x = newValue;
因此不必在普通的 redux reducer 中使用扩展运算符
return {
...state,
contents: state.contents.map(
(content, i) => i === 1 ? {...content, text: action.payload}
: content
)
}
您可以简单地重新分配本地值,让 Immer 为您处理剩下的事情:
state.contents[1].text = action.payload;
现场演示
注意数据结构:
在一个项目中我有这样的数据
state:{comments:{items:[{...},{...},{...},...]}
并在 items 中更新一个 item 我这样做
case actionTypes.UPDATE_COMMENT:
const indexComment = state.comments.items.findIndex(
(comment) => comment.id === action.payload.data.id,
);
return {
...state,
comments: {
...state.comments,
items: state.comments.items.map((el, index) =>
index === indexComment ? { ...el, ...action.payload.data } : el,
),
},
};
Immer.js(一个了不起的 react/rn/redux 友好包)非常有效地解决了这个问题。 redux 存储由不可变数据组成 - immer 允许您干净地更新存储的数据,就好像数据不是不可变的一样。
这是他们的 redux 文档中的示例:
(注意 produce() 包裹在该方法周围。这实际上是您的减速器设置中的唯一变化。)
import produce from "immer"
// Reducer with initial state
const INITIAL_STATE = [
/* bunch of todos */
]
const todosReducer = produce((draft, action) => {
switch (action.type) {
case "toggle":
const todo = draft.find(todo => todo.id === action.id)
todo.done = !todo.done
break
case "add":
draft.push({
id: action.id,
title: "A new todo",
done: false
})
break
default:
break
}
})
(有人提到 immer 是 redux-toolkit 的副作用,但你应该直接在你的 reducer 中使用 immer。)
我遇到了一个问题,即重新呈现状态会导致 ui 问题,并且建议只更新我的 reducer 中的特定值以减少页面上的重新呈现量。
这是我所在州的例子
{
name: "some name",
subtitle: "some subtitle",
contents: [
{title: "some title", text: "some text"},
{title: "some other title", text: "some other text"}
]
}
我目前正在这样更新它
case 'SOME_ACTION':
return { ...state, contents: action.payload }
其中 action.payload
是包含新值的整个数组。但现在我实际上只需要更新内容数组中第二项的文本,而这样的事情不起作用
case 'SOME_ACTION':
return { ...state, contents[1].text: action.payload }
其中 action.payload
现在是我需要更新的文本。
您不必在一行中完成所有操作:
case 'SOME_ACTION': {
const newState = { ...state };
newState.contents =
[
newState.contents[0],
{title: newState.contents[1].title, text: action.payload}
];
return newState
};
您可以使用 React Immutability helpers
import update from 'react-addons-update';
// ...
case 'SOME_ACTION':
return update(state, {
contents: {
1: {
text: {$set: action.payload}
}
}
});
虽然我想你可能会做更多类似的事情?
case 'SOME_ACTION':
return update(state, {
contents: {
[action.id]: {
text: {$set: action.payload}
}
}
});
您可以使用 map
。这是一个示例实现:
case 'SOME_ACTION':
return {
...state,
contents: state.contents.map(
(content, i) => i === 1 ? {...content, text: action.payload}
: content
)
}
派对已经很晚了,但这里有一个适用于每个索引值的通用解决方案。
您创建一个新数组并将其从旧数组扩展到您要更改的
index
。添加你想要的数据。
创建一个新数组并将其展开,从您要更改的
index
到数组末尾
let index=1;// probably action.payload.id
case 'SOME_ACTION':
return {
...state,
contents: [
...state.contents.slice(0,index),
{title: "some other title", text: "some other text"},
...state.contents.slice(index+1)
]
}
更新:
我做了一个小模块来简化代码,所以你只需要调用一个函数:
case 'SOME_ACTION':
return {
...state,
contents: insertIntoArray(state.contents,index, {title: "some title", text: "some text"})
}
有关更多示例,请查看 repository
函数签名:
insertIntoArray(originalArray,insertionIndex,newData)
编辑: 还有Immer.js库可以处理各种值,它们也可以深度嵌套。
我相信当你需要在你的 Redux 状态上进行这种操作时 传播运算符是你的朋友并且这个原则适用于所有 children.
让我们假装这是你的状态:
const state = {
houses: {
gryffindor: {
points: 15
},
ravenclaw: {
points: 18
},
hufflepuff: {
points: 7
},
slytherin: {
points: 5
}
}
}
你想给拉文克劳加3分
const key = "ravenclaw";
return {
...state, // copy state
houses: {
...state.houses, // copy houses
[key]: { // update one specific house (using Computed Property syntax)
...state.houses[key], // copy that specific house's properties
points: state.houses[key].points + 3 // update its `points` property
}
}
}
通过使用扩展运算符,您可以只更新新状态,而其他一切都保持不变。
示例取自此 amazing article,您可以找到几乎所有可能的选项以及很好的示例。
在我的例子中,我根据路易斯的回答做了这样的事情:
// ...State object...
userInfo = {
name: '...',
...
}
// ...Reducer's code...
case CHANGED_INFO:
return {
...state,
userInfo: {
...state.userInfo,
// I'm sending the arguments like this: changeInfo({ id: e.target.id, value: e.target.value }) and use them as below in reducer!
[action.data.id]: action.data.value,
},
};
这就是我为我的一个项目所做的:
const markdownSaveActionCreator = (newMarkdownLocation, newMarkdownToSave) => ({
type: MARKDOWN_SAVE,
saveLocation: newMarkdownLocation,
savedMarkdownInLocation: newMarkdownToSave
});
const markdownSaveReducer = (state = MARKDOWN_SAVED_ARRAY_DEFAULT, action) => {
let objTemp = {
saveLocation: action.saveLocation,
savedMarkdownInLocation: action.savedMarkdownInLocation
};
switch(action.type) {
case MARKDOWN_SAVE:
return(
state.map(i => {
if (i.saveLocation === objTemp.saveLocation) {
return Object.assign({}, i, objTemp);
}
return i;
})
);
default:
return state;
}
};
恐怕使用数组的 map()
方法可能会很昂贵,因为要迭代整个数组。相反,我组合了一个由三部分组成的新数组:
- head - 修改项 之前的项
- 修改项
- tail - 修改项 之后的项
这里是我在代码中使用的示例(NgRx,但其他 Redux 实现的机制相同):
// toggle done property: true to false, or false to true
function (state, action) {
const todos = state.todos;
const todoIdx = todos.findIndex(t => t.id === action.id);
const todoObj = todos[todoIdx];
const newTodoObj = { ...todoObj, done: !todoObj.done };
const head = todos.slice(0, todoIdx - 1);
const tail = todos.slice(todoIdx + 1);
const newTodos = [...head, newTodoObj, ...tail];
}
这在 redux-toolkit 中非常简单,它使用 Immer 帮助您编写看起来像可变的不可变代码,更简洁易读。
// it looks like the state is mutated, but under the hood Immer keeps track of
// every changes and create a new state for you
state.x = newValue;
因此不必在普通的 redux reducer 中使用扩展运算符
return {
...state,
contents: state.contents.map(
(content, i) => i === 1 ? {...content, text: action.payload}
: content
)
}
您可以简单地重新分配本地值,让 Immer 为您处理剩下的事情:
state.contents[1].text = action.payload;
现场演示
注意数据结构:
在一个项目中我有这样的数据
state:{comments:{items:[{...},{...},{...},...]}
并在 items 中更新一个 item 我这样做
case actionTypes.UPDATE_COMMENT:
const indexComment = state.comments.items.findIndex(
(comment) => comment.id === action.payload.data.id,
);
return {
...state,
comments: {
...state.comments,
items: state.comments.items.map((el, index) =>
index === indexComment ? { ...el, ...action.payload.data } : el,
),
},
};
Immer.js(一个了不起的 react/rn/redux 友好包)非常有效地解决了这个问题。 redux 存储由不可变数据组成 - immer 允许您干净地更新存储的数据,就好像数据不是不可变的一样。
这是他们的 redux 文档中的示例: (注意 produce() 包裹在该方法周围。这实际上是您的减速器设置中的唯一变化。)
import produce from "immer"
// Reducer with initial state
const INITIAL_STATE = [
/* bunch of todos */
]
const todosReducer = produce((draft, action) => {
switch (action.type) {
case "toggle":
const todo = draft.find(todo => todo.id === action.id)
todo.done = !todo.done
break
case "add":
draft.push({
id: action.id,
title: "A new todo",
done: false
})
break
default:
break
}
})
(有人提到 immer 是 redux-toolkit 的副作用,但你应该直接在你的 reducer 中使用 immer。)