在ngrx/store中更新对象

10

我正在为一个 Angular 2 应用程序使用 @ngrx/store。

我的 store 包含一系列的 Book 对象。我希望更新其中一个对象的字段。假设我有一个 Book 实例的 Observable,我想要进行更新操作(比如 selectedBook)。

为了进行更新,我打算使用一个 UpdateBookAction、以及新 Book 对象作为负载来调用 reducer。因此,我通过订阅 selectedBook 并调用 Object.assign() 来深度复制现有的 Book 对象。

但是当我尝试写入副本中的某个字段时,会出现以下错误(如果我尝试直接写入 store 中的 Book 对象,则会出现相同的错误)。

Error

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。从那里开始,一切都应该正常工作。我在 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);

}

但我们在这里不仅仅是谈论两个字段... 如果我的书有多个字段怎么办?我需要每次手动从头开始构建一个新的Book来更新一个字段吗?

我的解决方案是使用 Object.assign({}, book) 进行深度复制(而不是改变旧值!)并随后仅对我想要修改的字段进行更新。


你能发布一下书籍对象的定义吗?似乎可能是一个只写属性的问题。 - Halaster
1
是的,看到书的定义会很有帮助。当您在单个调用中完成所有操作时,是否遇到相同的问题?例如 Object.assign({}, book, { name: 'something else' }) - rob3c
4个回答

15
ngrx store的理念是只有一个单一的真实性来源,这意味着所有对象都是不可变的,而改变任何东西的唯一方法是将所有东西作为整体重新创建。此外,您可能正在使用ngrx freeze(https://github.com/codewareio/ngrx-store-freeze),这意味着所有对象都将被创建为只读,因此您将无法更改任何内容(如果您想完全遵循redux模式,则这是开发的好处)。如果删除store冻结对象的部分,则可以更改它,但这不是最佳实践。
我建议您使用ngrx observable和async管道将数据(在您的情况下是书籍)放入一个哑组件中,该组件只能获得输入并输出某些事件。然后,在哑组件内部,您可以通过复制它来“编辑”该对象,并在完成后将更改发回订阅store的智能组件,允许它通过store更改状态(提交)。这种方式是最好的,因为对于非常小的更改(例如双向绑定,当用户键入时...),很少更改整个状态。
如果您遵循redux模式,那么您将能够添加历史记录,这意味着存储库将保留最后X个状态重建的副本,因此您可以获得撤消功能、更容易调试、时间轴等。
您的问题是直接编辑属性而不是重新创建整个状态。

1
嗨Denko,感谢你抽出时间回复。我已经添加了澄清的评论。我知道保持状态不可变的目标。我并不是想改变状态本身,而只是想复制它,以便在传递负载之前调整副本。不过,我认为你对ngrx-store-freeze有所发现。我怀疑深层复制会带来对象的只读特性。这迫使我关闭ngrx-store-freeze,但我确信我将继续遵循“最佳实践”。这可能更多是ngrx-store-freeze的缺点,而不是其他什么问题。 - Daniel Patrick
嗨,丹尼尔,我对cloneDeep的担忧与你完全相同,即使已经过去了3年,它仍会复制只读属性。你有任何更新吗? - Tethys Zhang

9
我将假设OP实际遇到的情况。问题在于无法修改被冻结对象的成员,这是抛出的错误。原因是使用 `ngrx-store-freeze` 作为元Reducer来冻结进入store的任何对象。在另一个地方,当需要更改对象时,浅复制正在进行。`Object.assign()` 不进行深层复制。修改了从原始对象访问的另一个对象的成员。该次要对象也被冻结了,但未被复制。解决方案是使用像lodash中的 `cloneDeep()` 这样的深度复制方法。或者发送一些属性袋以通过适当的操作进行更改,并在Reducer上处理更改。

2

如前所述 - 您收到以下错误信息的原因是

Cannot assign to read only property 'name' of object

是因为 'ngrx-store-freeze' 冻结了状态并防止其突变。

Object.assign 将提供一个新对象,就像您期望的那样,但是它将复制状态的属性以及每个属性自己的定义 - 例如 'writable' 定义('ngrx-store-freeze' 可能将其设置为 false)。

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

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


1
一种实现方法是使用一个实用程序/助手方法来创建新书。您可以提供现有书籍和要添加到新书中的属性子集(如果想要类型安全,可以在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: '先是foo,然后是bar'});来调用它,并使用这个新建的book对象来更新你的存储。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接