如何添加/删除使用 normalizr 生成的 redux 存储?
Posted
技术标签:
【中文标题】如何添加/删除使用 normalizr 生成的 redux 存储?【英文标题】:How do you add/remove to a redux store generated with normalizr? 【发布时间】:2016-04-29 12:38:30 【问题描述】:查看README中的示例:
鉴于“坏”结构:
[
id: 1,
title: 'Some Article',
author:
id: 1,
name: 'Dan'
,
id: 2,
title: 'Other Article',
author:
id: 1,
name: 'Dan'
]
添加新对象非常容易。我所要做的就是像
return
...state,
myNewObject
在减速器中。
现在考虑到“好”树的结构,我不知道应该如何处理它。
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 树中添加/删除吗?
【问题讨论】:
【参考方案1】:以下内容直接来自redux/normalizr创建者here的帖子:
所以你的状态应该是这样的:
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 相互引用。因此,每当某些对象发生变化时,只有一个地方需要更新。
其次,请注意我们对 CREATE_PLAN 的反应是如何在计划减速器中添加适当的实体并将其 ID 添加到 currentPlans 减速器中。这个很重要。在更复杂的应用程序中,您可能有关系,例如计划减速器可以通过将新 ID 附加到计划内的数组来以相同的方式处理 ADD_EXERCISE_TO_PLAN。但是如果练习本身更新了,计划reducer就不需要知道这一点,因为ID没有改变。
第三,注意实体化简器(计划和练习)有特殊子句注意 action.entities。这是为了以防我们有一个带有“已知事实”的服务器响应,我们想要更新我们所有的实体来反映。要在分派操作之前以这种方式准备数据,您可以使用 normalizr。你可以在 Redux repo 的“真实世界”示例中看到它的使用。
最后,请注意实体化简器的相似之处。您可能想编写一个函数来生成这些。这超出了我的回答范围——有时你想要更多的灵活性,有时你想要更少的样板。您可以查看“真实世界”示例减速器中的分页代码,以获取生成类似减速器的示例。
哦,我使用了 ...a, ...b 语法。它在 Babel 阶段 2 中作为 ES7 提案启用。它被称为“对象扩展运算符”,相当于编写 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)
;
【讨论】:
感谢@AR7 的精彩解释。我有一个问题:为什么我们需要将 currentPlans 数组保持在状态并保持更新(好吧,如果你有它的状态,当然,至少要更新它,但它在其他地方有什么用) ?在该州拥有计划的对象还不够吗?它在实践中是用来做什么的?我注意到 Redux 文档以及 normalizr 文档都提到了这些数组。 @Cedric 从我的角度来看,它用于保持对象的顺序。 HashMap 没有顺序,因此如果您只保留计划对象,则每次刷新页面时,顺序可能会完全不同。此外,您不能在任何 MVC 框架中迭代对象,因此您需要在 react 中执行Object.keys(plans).map()
之类的操作,而不仅仅是使用当前的计划数组。
很好的解释!那你要怎么删? ...state, [action.id]: undefined ?
@NikSo 这正是我在这里的原因.....没有我在哪里看到任何提到从规范化商店中删除实体的惯用方法?我很难相信我们是唯一的人......你有没有深究?
@NikSo 您可以分多个步骤完成。比如const newState = ...state
,然后是delete newState[action.id]
,然后是return newState
。如果您不改变旧状态,则可以进行突变。【参考方案2】:
大多数时候,我对从 API 获取的数据使用 normalizr,因为我无法控制(通常)深层嵌套的数据结构。让我们区分实体和结果及其用法。
实体
所有纯数据都在规范化后的实体对象中(在您的情况下为articles
和users
)。我建议要么对所有实体使用 reducer,要么对每个实体类型使用 reducer。实体化简器应负责使您的(服务器)数据保持同步并拥有单一的事实来源。
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 的文章)。您可以轻松地将它们保存在另一个特定于用例的 reducer 中。该 reducer 的状态可能如下所示:
const state =
recommended: ['1', '2' ],
articlesByAuthor:
X: ['2'],
,
;
现在您可以很容易地看到作者 X 的文章也是推荐文章。但是你在你的文章实体化简器中只保留了一个单一的事实来源。
在您的组件中,您可以简单地映射实体 + 推荐的 /articlesByAuthor 来呈现实体。
免责声明:我可以推荐我写的一篇博文,它展示了一个真实世界的应用程序如何使用 normalizr 来防止状态管理出现问题:Redux Normalizr: Improve your State Management
【讨论】:
【参考方案3】:我已经实现了一个可以在互联网上找到的通用减速器的小偏差。它能够从缓存中删除项目。您所要做的就是确保在每次删除时发送一个带有已删除字段的操作:
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],
)
【讨论】:
【参考方案4】:派对迟到了几年,但这里开始了——
您可以使用normalized-reducer 轻松管理规范化的reducer 状态,无需样板。 你传入一个描述关系的模式,它会返回 reducer、actions 和 selectors 来管理那个状态片。
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 逻辑以及更复杂的逻辑,例如关系附件/分离、级联删除和批处理操作。
继续这个例子,状态看起来像:
"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 集成
【讨论】:
【参考方案5】:在您的 reducer 中,保留一份未标准化数据的副本。这样,您可以执行以下操作(将新对象添加到状态数组时):
case ACTION:
return
unNormalizedData: [...state.unNormalizedData, action.data],
normalizedData: normalize([...state.unNormalizedData, action.data], normalizrSchema),
如果您不想在存储中保留未规范化的数据,也可以使用denormalize
【讨论】:
这里的主要危险信号。首先,您应该避免在商店中重复数据。它在自找麻烦,是一种代码味道。此外,reducer 应该尽可能精简,并且不推荐在每个周期调用 normalize。 当您使用复杂模式进行规范化时,您会如何建议更新/删除。例如,idAttribute 是一个函数并且使用了 process 和 merge 策略?这种方法非常简单明了,从未对我造成任何性能问题。 如果您对规范化数据进行修改,现在非规范化的重复数据(“unNormalizedData”)已过期。 我建议遵循存储平面、标准化数据并在 reducer 中更新数据的标准。然后在你的 UI 组件中使用 denormalize()。以上是关于如何添加/删除使用 normalizr 生成的 redux 存储?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 normalizr 解析 FractalTransformer
如何使用 normalizr 规范化来自 JSON 的数据?
如何使用“normalizr”规范化这个简单的 API 响应?