我正在使用@ ngrx / store作为Angular 2应用程序。
我的商店拥有一个说明Book
个对象的列表。我想更新其中一个对象中的字段。我也碰巧有一个我想要更新的Book实例的Observable(比如,selectedBook
)。
要进行更新,我打算使用UpdateBookAction
调用reducer,以及新Book的有效负载。所以我通过订阅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)
进行深层复制(而不是改变旧版本!),然后仅对我想要触摸的字段进行更新。
答案 0 :(得分:7)
ngrx商店的想法是拥有一个且只有一个单一的真实位置,这意味着所有对象都是不可变的,而改变任何东西的唯一方法就是重新创建整个事物。此外,您可能正在使用ngrx冻结(https://github.com/codewareio/ngrx-store-freeze),这意味着所有对象都将以只读方式创建,因此您无法更改任何对象(如果您想要完全遵循redux模式)。如果您删除商店冻结对象的部分,您将能够更改它,但这不是最佳实践。
我建议您使用以下内容:使用带有异步管道的ngrx observable将数据(在您的案例中)放入一个只能输入和输出某个事件的哑组件中。在愚蠢的组件内,你可以&#34;编辑&#34;该对象通过制作它的副本,并在完成后,您可以将更改发回到订阅商店的智能组件,并允许它通过存储(提交)更改状态。这种方式是最好的,因为对于一个非常小的更改来改变整个状态并不常见(比如当用户输入时双向绑定...)。
如果您遵循redux模式,那么您将能够添加历史记录,这意味着商店将保留最后X个状态重新创建的副本,因此您可以获得UNDO功能,更容易调试,时间线等
您的问题是您直接编辑属性而不是重新创建整个状态。
答案 1 :(得分:6)
我必须对OP正在经历的实际情况做出假设。
问题
无法修改冻结对象的成员。它的错误被抛出。
原因
Object.assign()
用作元缩减器来冻结进入商店的任何对象。在另一个地方,当需要更改对象时,正在进行浅拷贝。 cloneDeep()
不进行深层复制。正在修改从原始对象到达的另一个对象的成员。此辅助对象也被冻结,不会重复。
<强>解决方案强>
使用lodash中的$command1 = exec("mail -s 'title' info@example.com <<< 'message'");
$text1 = file_get_contents('http://example.com/page');
$intext1 = strpos($text1, 'tvshenja1') !== false; // note !==, not !=
echo $intext1 ? 'do nothing' : $command1;
这样的深层副本。或者通过适当的行动发送一袋待更改的房产。在reducer上处理更改。
答案 2 :(得分:1)
正如已经提到的 - 你得到的原因
无法指定只读属性&#39; name&#39;对象
是因为&#39; ngrx-store-freeze&#39;冻结国家,防止变异。
Object.assign将提供您期望的新对象,但它将复制州的属性以及每个属性自己的定义 - 例如&#39 ;可写&#39;定义(&#39; ngrx-store-freeze&#39;可能设置为false)。
描述了一种不同的方法in this answer并解释了如何使用JSON.parse(JSON.stringify(yourObject))克隆对象的速度最快,但如果你保留日期或方法等,这种方法就有缺陷。在你的州。
使用lodash&#39; cloneDeep&#39;对于深度克隆国家来说,这可能是你最好的选择。
答案 3 :(得分:1)
实现此目的的一种方法是使用实用程序/帮助程序方法来制作新书。
您可以给它一本现有的书,以及要添加到新书中的属性的子集(如果要输入安全性,请在Partial
中使用typeScript
。
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'});
进行调用
并使用此newBook更新您的商店。