你如何 add/remove 到一个用 normalizr 生成的 redux store?
How do you add/remove to a redux store generated with normalizr?
查看 README 中的示例:
给定 "bad" 结构:
[{
id: 1,
title: 'Some Article',
author: {
id: 1,
name: 'Dan'
}
}, {
id: 2,
title: 'Other Article',
author: {
id: 1,
name: 'Dan'
}
}]
添加新对象非常容易。我所要做的就是
return {
...state,
myNewObject
}
在减速器中。
现在给定 "good" 树的结构,我不知道应该如何处理它。
{
result: [1, 2],
entities: {
articles: {
1: {
id: 1,
title: 'Some Article',
author: 1
},
2: {
id: 2,
title: 'Other Article',
author: 1
}
},
users: {
1: {
id: 1,
name: 'Dan'
}
}
}
}
我想到的每一种方法都需要一些复杂的对象操作,这让我觉得我的方向不对,因为 normalizr 应该会让我的生活更轻松。
我在网上找不到任何人以这种方式使用 normalizr 树的例子。 The official example 没有添加和删除,所以也没有帮助。
有人可以让我知道如何以正确的方式从 normalizr 树 add/remove 吗?
以下直接来自 redux/normalizr 创建者 here 的 post:
所以你的状态看起来像:
{
entities: {
plans: {
1: {title: 'A', exercises: [1, 2, 3]},
2: {title: 'B', exercises: [5, 1, 2]}
},
exercises: {
1: {title: 'exe1'},
2: {title: 'exe2'},
3: {title: 'exe3'}
}
},
currentPlans: [1, 2]
}
你的减速器可能看起来像
import merge from 'lodash/object/merge';
const exercises = (state = {}, action) => {
switch (action.type) {
case 'CREATE_EXERCISE':
return {
...state,
[action.id]: {
...action.exercise
}
};
case 'UPDATE_EXERCISE':
return {
...state,
[action.id]: {
...state[action.id],
...action.exercise
}
};
default:
if (action.entities && action.entities.exercises) {
return merge({}, state, action.entities.exercises);
}
return state;
}
}
const plans = (state = {}, action) => {
switch (action.type) {
case 'CREATE_PLAN':
return {
...state,
[action.id]: {
...action.plan
}
};
case 'UPDATE_PLAN':
return {
...state,
[action.id]: {
...state[action.id],
...action.plan
}
};
default:
if (action.entities && action.entities.plans) {
return merge({}, state, action.entities.plans);
}
return state;
}
}
const entities = combineReducers({
plans,
exercises
});
const currentPlans = (state = [], action) {
switch (action.type) {
case 'CREATE_PLAN':
return [...state, action.id];
default:
return state;
}
}
const reducer = combineReducers({
entities,
currentPlans
});
这是怎么回事?首先,注意状态是标准化的。我们永远不会在其他实体中拥有实体。相反,它们通过 ID 相互引用。因此,每当某个对象发生变化时,只有一个地方需要更新。
其次,请注意我们如何通过在 plans reducer 中添加适当的实体并将其 ID 添加到 currentPlans reducer 来对 CREATE_PLAN 作出反应。这个很重要。在更复杂的应用程序中,您可能有关系,例如计划 reducer 可以通过将新 ID 附加到计划内的数组,以相同的方式处理 ADD_EXERCISE_TO_PLAN。但是如果练习本身被更新了,那么 plans reducer 就不需要知道了,因为 ID 没有改变。
第三,请注意实体减速器(计划和练习)有特殊条款注意 action.entities。这是为了防止我们有一个“已知真相”的服务器响应,我们想要更新我们所有的实体来反映。要在调度操作之前以这种方式准备数据,您可以使用 normalizr。您可以在 Redux 存储库的“真实世界”示例中看到它的使用。
最后,请注意实体减速器的相似之处。您可能想编写一个函数来生成它们。这超出了我的回答范围——有时您想要更多的灵活性,有时您想要更少的样板文件。您可以查看“真实世界”示例缩减器中的分页代码,以获取生成类似缩减器的示例。
哦,我使用了 { ...a, ...b } 语法。它作为 ES7 提案在 Babel stage 2 中启用。它被称为“对象展开运算符”,相当于写作 Object.assign({}, a, b).
至于库,你可以使用 Lodash(注意不要改变,例如 merge({}, a, b} 是正确的,但 merge(a, b) 不是),updeep,react-addons-update 或其他东西。但是,如果您发现自己需要进行深度更新,则可能意味着您的状态树不够平坦,并且您没有充分利用功能组合。即使是您的第一个示例:
case 'UPDATE_PLAN':
return {
...state,
plans: [
...state.plans.slice(0, action.idx),
Object.assign({}, state.plans[action.idx], action.plan),
...state.plans.slice(action.idx + 1)
]
};
可以写成
const plan = (state = {}, action) => {
switch (action.type) {
case 'UPDATE_PLAN':
return Object.assign({}, state, action.plan);
default:
return state;
}
}
const plans = (state = [], action) => {
if (typeof action.idx === 'undefined') {
return state;
}
return [
...state.slice(0, action.idx),
plan(state[action.idx], action),
...state.slice(action.idx + 1)
];
};
// somewhere
case 'UPDATE_PLAN':
return {
...state,
plans: plans(state.plans, action)
};
大多数时候,我对从 API 获取的数据使用 normalizr,因为我无法控制(通常)深层嵌套的数据结构。让我们区分实体和结果及其用法。
实体
所有纯数据在规范化后都在实体对象中(在您的情况下 articles
和 users
)。我建议为所有实体使用一个缩减器,或者为每个实体类型使用一个缩减器。实体缩减程序应负责保持您的(服务器)数据同步并拥有单一的真实来源。
const initialState = {
articleEntities: {},
userEntities: {},
};
结果
结果只是对您的实体的引用。想象一下以下场景:(1) 您从 API 推荐的 articles
和 ids: ['1', '2']
中获取。您将实体保存在 article entity reducer 中。 (2) 现在你用 id: 'X'
获取特定作者写的所有文章。您再次同步 article entity reducer 中的文章。 article entity reducer 是所有文章数据的唯一真实来源 - 仅此而已。现在您想要另一个地方来区分文章((1) 推荐文章和 (2) 作者 X 的文章)。您可以轻松地将它们保存在另一个用例特定的减速器中。该减速器的状态可能如下所示:
const state = {
recommended: ['1', '2' ],
articlesByAuthor: {
X: ['2'],
},
};
现在很容易看出作者X的文章也是推荐文章。但是你在你的文章实体缩减器中只保留一个单一的真实来源。
在您的组件中,您可以简单地映射实体 + recommended /articlesByAuthor 来呈现实体。
免责声明:我可以推荐我写的一篇博客 post,它展示了真实世界的应用程序如何使用 normalizr 来防止状态管理中的问题:Redux Normalizr: Improve your State Management
我实现了一个可以在 Internet 上找到的通用减速器的小偏差。它能够从缓存中删除项目。您所要做的就是确保在每次删除时发送一个包含已删除字段的操作:
export default (state = entities, action) => {
if (action.response && action.response.entities)
state = merge(state, action.response.entities)
if (action.deleted) {
state = {...state}
Object.keys(action.deleted).forEach(entity => {
let deleted = action.deleted[entity]
state[entity] = Object.keys(state[entity]).filter(key => !deleted.includes(key))
.reduce((p, id) => ({...p, [id]: state[entity][id]}), {})
})
}
return state
}
操作代码中的用法示例:
await AlarmApi.remove(alarmId)
dispatch({
type: 'ALARM_DELETED',
alarmId,
deleted: {alarms: [alarmId]},
})
在你的 reducer 中,保留一份非规范化数据的副本。这样,你可以做这样的事情(当添加一个新对象到状态数组时):
case ACTION:
return {
unNormalizedData: [...state.unNormalizedData, action.data],
normalizedData: normalize([...state.unNormalizedData, action.data], normalizrSchema),
}
如果您不想在商店中保留未规范化的数据,您也可以使用 denormalize
晚会晚了很多年,但现在开始了 —
您可以使用 normalized-reducer 轻松管理规范化的 reducer 状态,无需样板文件。
您传入一个描述关系的模式,它会返回缩减器、操作和选择器来管理该状态片。
import makeNormalizedSlice from 'normalized-reducer';
const schema = {
user: {
articles: {
type: 'article', cardinality: 'many', reciprocal: 'author'
}
},
article: {
author: {
type: 'user', cardinality: 'one', reciprocal: 'articles'
}
}
};
const {
actionCreators,
selectors,
reducer,
actionTypes,
emptyState
} = makeNormalizedSlice(schema);
这些操作允许您执行基本的 CRUD 逻辑以及更复杂的逻辑,例如关系 attachments/detachments、级联删除和批处理操作。
继续这个例子,状态看起来像:
{
"entities": {
"user": {
"1": {
"id": "1",
"name": "Dan",
"articles": ["1", "2"]
}
},
"article": {
"1": {
"id": "1",
"author": "1",
"title": "Some Article",
},
"2": {
"id": "2",
"author": "1",
"title": "Other Article",
}
}
},
"ids": {
"user": ["1"],
"article": ["1", "2"]
}
}
Normalized Reducer 还集成了 normalizr:
import { normalize } from 'normalizr'
import { fromNormalizr } from 'normalized-reducer'
const denormalizedData = {...}
const normalizrSchema = {...}
const normalizedData = normalize(denormalizedData, normalizrSchema);
const initialState = fromNormalizr(normalizedData);
Another example normalizr 集成
查看 README 中的示例:
给定 "bad" 结构:
[{
id: 1,
title: 'Some Article',
author: {
id: 1,
name: 'Dan'
}
}, {
id: 2,
title: 'Other Article',
author: {
id: 1,
name: 'Dan'
}
}]
添加新对象非常容易。我所要做的就是
return {
...state,
myNewObject
}
在减速器中。
现在给定 "good" 树的结构,我不知道应该如何处理它。
{
result: [1, 2],
entities: {
articles: {
1: {
id: 1,
title: 'Some Article',
author: 1
},
2: {
id: 2,
title: 'Other Article',
author: 1
}
},
users: {
1: {
id: 1,
name: 'Dan'
}
}
}
}
我想到的每一种方法都需要一些复杂的对象操作,这让我觉得我的方向不对,因为 normalizr 应该会让我的生活更轻松。
我在网上找不到任何人以这种方式使用 normalizr 树的例子。 The official example 没有添加和删除,所以也没有帮助。
有人可以让我知道如何以正确的方式从 normalizr 树 add/remove 吗?
以下直接来自 redux/normalizr 创建者 here 的 post:
所以你的状态看起来像:
{
entities: {
plans: {
1: {title: 'A', exercises: [1, 2, 3]},
2: {title: 'B', exercises: [5, 1, 2]}
},
exercises: {
1: {title: 'exe1'},
2: {title: 'exe2'},
3: {title: 'exe3'}
}
},
currentPlans: [1, 2]
}
你的减速器可能看起来像
import merge from 'lodash/object/merge';
const exercises = (state = {}, action) => {
switch (action.type) {
case 'CREATE_EXERCISE':
return {
...state,
[action.id]: {
...action.exercise
}
};
case 'UPDATE_EXERCISE':
return {
...state,
[action.id]: {
...state[action.id],
...action.exercise
}
};
default:
if (action.entities && action.entities.exercises) {
return merge({}, state, action.entities.exercises);
}
return state;
}
}
const plans = (state = {}, action) => {
switch (action.type) {
case 'CREATE_PLAN':
return {
...state,
[action.id]: {
...action.plan
}
};
case 'UPDATE_PLAN':
return {
...state,
[action.id]: {
...state[action.id],
...action.plan
}
};
default:
if (action.entities && action.entities.plans) {
return merge({}, state, action.entities.plans);
}
return state;
}
}
const entities = combineReducers({
plans,
exercises
});
const currentPlans = (state = [], action) {
switch (action.type) {
case 'CREATE_PLAN':
return [...state, action.id];
default:
return state;
}
}
const reducer = combineReducers({
entities,
currentPlans
});
这是怎么回事?首先,注意状态是标准化的。我们永远不会在其他实体中拥有实体。相反,它们通过 ID 相互引用。因此,每当某个对象发生变化时,只有一个地方需要更新。
其次,请注意我们如何通过在 plans reducer 中添加适当的实体并将其 ID 添加到 currentPlans reducer 来对 CREATE_PLAN 作出反应。这个很重要。在更复杂的应用程序中,您可能有关系,例如计划 reducer 可以通过将新 ID 附加到计划内的数组,以相同的方式处理 ADD_EXERCISE_TO_PLAN。但是如果练习本身被更新了,那么 plans reducer 就不需要知道了,因为 ID 没有改变。
第三,请注意实体减速器(计划和练习)有特殊条款注意 action.entities。这是为了防止我们有一个“已知真相”的服务器响应,我们想要更新我们所有的实体来反映。要在调度操作之前以这种方式准备数据,您可以使用 normalizr。您可以在 Redux 存储库的“真实世界”示例中看到它的使用。
最后,请注意实体减速器的相似之处。您可能想编写一个函数来生成它们。这超出了我的回答范围——有时您想要更多的灵活性,有时您想要更少的样板文件。您可以查看“真实世界”示例缩减器中的分页代码,以获取生成类似缩减器的示例。
哦,我使用了 { ...a, ...b } 语法。它作为 ES7 提案在 Babel stage 2 中启用。它被称为“对象展开运算符”,相当于写作 Object.assign({}, a, b).
至于库,你可以使用 Lodash(注意不要改变,例如 merge({}, a, b} 是正确的,但 merge(a, b) 不是),updeep,react-addons-update 或其他东西。但是,如果您发现自己需要进行深度更新,则可能意味着您的状态树不够平坦,并且您没有充分利用功能组合。即使是您的第一个示例:
case 'UPDATE_PLAN':
return {
...state,
plans: [
...state.plans.slice(0, action.idx),
Object.assign({}, state.plans[action.idx], action.plan),
...state.plans.slice(action.idx + 1)
]
};
可以写成
const plan = (state = {}, action) => {
switch (action.type) {
case 'UPDATE_PLAN':
return Object.assign({}, state, action.plan);
default:
return state;
}
}
const plans = (state = [], action) => {
if (typeof action.idx === 'undefined') {
return state;
}
return [
...state.slice(0, action.idx),
plan(state[action.idx], action),
...state.slice(action.idx + 1)
];
};
// somewhere
case 'UPDATE_PLAN':
return {
...state,
plans: plans(state.plans, action)
};
大多数时候,我对从 API 获取的数据使用 normalizr,因为我无法控制(通常)深层嵌套的数据结构。让我们区分实体和结果及其用法。
实体
所有纯数据在规范化后都在实体对象中(在您的情况下 articles
和 users
)。我建议为所有实体使用一个缩减器,或者为每个实体类型使用一个缩减器。实体缩减程序应负责保持您的(服务器)数据同步并拥有单一的真实来源。
const initialState = {
articleEntities: {},
userEntities: {},
};
结果
结果只是对您的实体的引用。想象一下以下场景:(1) 您从 API 推荐的 articles
和 ids: ['1', '2']
中获取。您将实体保存在 article entity reducer 中。 (2) 现在你用 id: 'X'
获取特定作者写的所有文章。您再次同步 article entity reducer 中的文章。 article entity reducer 是所有文章数据的唯一真实来源 - 仅此而已。现在您想要另一个地方来区分文章((1) 推荐文章和 (2) 作者 X 的文章)。您可以轻松地将它们保存在另一个用例特定的减速器中。该减速器的状态可能如下所示:
const state = {
recommended: ['1', '2' ],
articlesByAuthor: {
X: ['2'],
},
};
现在很容易看出作者X的文章也是推荐文章。但是你在你的文章实体缩减器中只保留一个单一的真实来源。
在您的组件中,您可以简单地映射实体 + recommended /articlesByAuthor 来呈现实体。
免责声明:我可以推荐我写的一篇博客 post,它展示了真实世界的应用程序如何使用 normalizr 来防止状态管理中的问题:Redux Normalizr: Improve your State Management
我实现了一个可以在 Internet 上找到的通用减速器的小偏差。它能够从缓存中删除项目。您所要做的就是确保在每次删除时发送一个包含已删除字段的操作:
export default (state = entities, action) => {
if (action.response && action.response.entities)
state = merge(state, action.response.entities)
if (action.deleted) {
state = {...state}
Object.keys(action.deleted).forEach(entity => {
let deleted = action.deleted[entity]
state[entity] = Object.keys(state[entity]).filter(key => !deleted.includes(key))
.reduce((p, id) => ({...p, [id]: state[entity][id]}), {})
})
}
return state
}
操作代码中的用法示例:
await AlarmApi.remove(alarmId)
dispatch({
type: 'ALARM_DELETED',
alarmId,
deleted: {alarms: [alarmId]},
})
在你的 reducer 中,保留一份非规范化数据的副本。这样,你可以做这样的事情(当添加一个新对象到状态数组时):
case ACTION:
return {
unNormalizedData: [...state.unNormalizedData, action.data],
normalizedData: normalize([...state.unNormalizedData, action.data], normalizrSchema),
}
如果您不想在商店中保留未规范化的数据,您也可以使用 denormalize
晚会晚了很多年,但现在开始了 —
您可以使用 normalized-reducer 轻松管理规范化的 reducer 状态,无需样板文件。 您传入一个描述关系的模式,它会返回缩减器、操作和选择器来管理该状态片。
import makeNormalizedSlice from 'normalized-reducer';
const schema = {
user: {
articles: {
type: 'article', cardinality: 'many', reciprocal: 'author'
}
},
article: {
author: {
type: 'user', cardinality: 'one', reciprocal: 'articles'
}
}
};
const {
actionCreators,
selectors,
reducer,
actionTypes,
emptyState
} = makeNormalizedSlice(schema);
这些操作允许您执行基本的 CRUD 逻辑以及更复杂的逻辑,例如关系 attachments/detachments、级联删除和批处理操作。
继续这个例子,状态看起来像:
{
"entities": {
"user": {
"1": {
"id": "1",
"name": "Dan",
"articles": ["1", "2"]
}
},
"article": {
"1": {
"id": "1",
"author": "1",
"title": "Some Article",
},
"2": {
"id": "2",
"author": "1",
"title": "Other Article",
}
}
},
"ids": {
"user": ["1"],
"article": ["1", "2"]
}
}
Normalized Reducer 还集成了 normalizr:
import { normalize } from 'normalizr'
import { fromNormalizr } from 'normalized-reducer'
const denormalizedData = {...}
const normalizrSchema = {...}
const normalizedData = normalize(denormalizedData, normalizrSchema);
const initialState = fromNormalizr(normalizedData);
Another example normalizr 集成