更新 ngrx/store 中的对象

Posted

技术标签:

【中文标题】更新 ngrx/store 中的对象【英文标题】:Updating an object in the ngrx/store 【发布时间】:2017-04-15 10:54:49 【问题描述】:

我将 @ngrx/store 用于 Angular 2 应用程序。

我的商店有一个 Book 对象列表。我想更新其中一个对象中的字段。我也碰巧有一个我想要更新的 Book 实例的 Observable(比如,selectedBook)。

为了进行更新,我打算使用UpdateBookAction 和新书的有效负载调用reducer。因此,我通过订阅 selectedBook 然后调用 Object.assign() 对现有 Book 对象进行深拷贝。

但是当我尝试写入副本的其中一个字段时,我收到以下错误。 (如果我尝试直接写入商店中的 Book 对象,这恰好是同样的错误。)

错误

Cannot assign to read only property 'name' of object '#<Object>' at ViewWrappedError.BaseError [as constructor]

代码

ngOnInit() 
    this.book$ = this.store.let(fromRoot.getSelectedBook);
    //...


someFunction() 
    //...
    this.book$.subscribe(book => 

        let updatedBook = Object.assign(, book);
        updatedBook.name = 'something else';          // <--- THIS IS WHAT THROWS

        let action = new BookUpdateAction(updatedBook);
        this.store.dispatch(action);

    

评论后的澄清

我假设我可以使用不是商店的整个状态的有效负载进行操作。 (事实上​​,这似乎是必要的,不是吗?)鉴于文档,我相信情况就是如此。

我希望采取的行动是这样的:

Action = UPDATE, payload = 'id': 1234, 'name': 'something new'

如前所述,我打算这样打电话:

this.store.dispatch(action);

大概在幕后,ngrx 正在将我的操作与(不可变的)当前状态一起传递给减速器。

所以从那里开始,一切都应该可以正常工作。我在 reducer 中的逻辑不会改变现有状态,它只是从现有状态和我传入的有效负载中创建一个新状态。

这里真正的问题是如何合理地构建新的“objectToUpdate”,以便可以将其作为有效负载传递。

我可以这样做:

this.book$.subscribe(book => 

    let updatedBook = new Book();
    updatedBook.id = book.id;
    //set all other fields manually...
    updatedBook.name = 'something else';

    let action = new BookUpdateAction(updatedBook);
    this.store.dispatch(action);


但我们在这里讨论的不仅仅是两个领域……如果我的书有多个领域怎么办?我是否必须每次都从头开始手动构建一本新书才能更新一个字段?

我的解决方案是使用 Object.assign(, book) 进行深层复制(而不是改变旧的!),然后仅对我希望接触的字段进行更新。

【问题讨论】:

能否贴出 book 对象的定义?似乎这可能是只写属性的问题。 是的,看看书的定义会很有帮助。在一次通话中完成所有操作时,您是否有同样的问题?例如Object.assign(, book, name: 'something else' )? 【参考方案1】:

ngrx 存储的想法是拥有一个且只有一个真实的位置,这意味着所有对象都是不可变的,改变任何事物的唯一方法是重新创建一个整体。此外,您可能正在使用 ngrx freeze (https://github.com/codewareio/ngrx-store-freeze),这意味着所有对象都将以只读方式创建,因此您将无法更改任何对象(如果您想完全遵循 redux 模式,这对开发很有好处)。如果您删除商店冻结对象的部分,您将能够更改它,但这不是最佳做法。

我的建议如下:使用带有异步管道的 ngrx observable 将数据(在您的案例书中)放入一个只能获取输入和输出某些事件的哑组件中。然后,在哑组件内部,您可以通过复制该对象来“编辑”该对象,完成后,您可以将更改发送回订阅存储的智能组件并允许它更改状态通过商店(提交)。这种方式是最好的,因为改变整个状态以进行非常小的更改并不常见(例如两种方式绑定,当用户键入时......)。

如果您遵循 redux 模式,您将能够添加历史记录,这意味着商店将保留最后 X 状态重新创建的副本,因此您可以获得 UNDO 功能、更易于调试、时间线等

您的问题是您直接编辑属性而不是重新创建整个状态。

【讨论】:

您好 Denko,感谢您抽出宝贵时间回复。我在上面添加了澄清 cmets。我知道保持状态不可变的目标。我不是试图改变状态本身,只是试图制作它的副本,以便我可以在将其作为有效负载传递之前调整副本。我认为您使用 ngrx-store-freeze 有所收获。我怀疑深拷贝带来了对象的只读性质。这迫使我关闭 ngrx-store-freeze,但我相信我会继续遵循“最佳实践”。这可能更像是 ngrx-store-freeze 的一个缺点。 嗨,Daniel,我对 cloneDeep 有同样的担忧,它会复制 readonly 属性,尽管 3 年过去了。您对此有任何更新吗?【参考方案2】:

我必须对 OP 所经历的实际情况做出假设。

问题

无法修改冻结对象的成员。它是抛出的错误。

原因

ngrx-store-freeze 用作元减速器来冻结任何进入商店的对象。在另一个地方,当需要更改对象时,正在制作浅拷贝。 Object.assign() 不做深拷贝。正在修改从原始对象到达的另一个对象的成员。这个次要对象也被冻结了,因为它没有被复制

解决方案

使用来自 lodash 的深层副本,例如 cloneDeep()。或发送一袋属性以通过适当的操作进行更改。处理 reducer 上的更改。

【讨论】:

【参考方案3】:

正如已经提到的 - 你得到的原因

无法分配给对象的只读属性“名称”

是因为 'ngrx-store-freeze' 冻结状态并防止对其进行变异。

Object.assign 将如您所愿提供一个新对象,它将复制状态的属性以及每个属性自己的定义 - 例如“可写”定义(其中 'ngrx-store- freeze' 可能设置为 false)。

in this answer 描述了另一种方法,并解释了如何以最快的速度使用 JSON.parse(JSON.stringify(yourObject)) 克隆对象,但如果您将日期或方法等保留在您的状态中,则此方法存在缺陷。

使用 lodash 的 'cloneDeep' 可能是深度克隆状态的最佳选择。

【讨论】:

【参考方案4】:

实现这一点的一种方法是使用实​​用程序/辅助方法来制作一本新书。 你可以给它一本现有的书和你想添加到新书的属性子集(如果你想要类型安全,使用typeScript中的Partial)。

createNewBook(oldBook: Book, newProps: Partial<Book>): Book 
    const newBook = new Book(); 
    for(const prop in oldBook) 
        if(newProps[prop]) 
            newBook[prop]=newProps[prop];
         else 
            newBook[prop]=oldBook[prop];
        
    
    return newBook 

您可以通过newBook = createNewBook(new Book(), title: 'first foo, then bar'); 调用它 并使用这本新书更新您的商店。

【讨论】:

以上是关于更新 ngrx/store 中的对象的主要内容,如果未能解决你的问题,请参考以下文章

将 ngrx/Store 集成到现有的 Angular 项目中

Ngrx Store 作为提供者中的依赖关系导致循环依赖

发生未处理的异常:不支持:关键字“id”,使用“$id”作为模式 ID - Angular 13 中的“ng add @ngrx/store”

安装@ngrx/store 时如何清除错误?

如何重置ngrx/store的所有状态?

无法使 @ngrx/store 正常工作