Redux 如何在没有无用的浅拷贝的情况下不可变地更新嵌套状态对象?

Redux how to update nested state object immutably without useless shallow copies?

我在 angular 项目上使用 NGRX 商店。

这是状态类型:

export interface TagsMap {
    [key:number]: { // lets call this key - user id.
        [key: number]: number // and this key - tag id.
    }
}

例如:

{5: {1: 1, 2: 2}, 6: {3: 3}}

用户 5 的标签为:1,2,用户 6 的标签为 3。

我在这种状态下有超过 20 万个密钥,并希望尽可能提高更新效率。 该操作是为状态下的所有用户添加标签。 我尝试了这样的最佳实践方法:

const addTagToAllUsers = (state, tagIdToAdd) => {
  const userIds = Object.keys(state.userTags);
  return userIds.reduce((acc, contactId, index) => {
    return {
      ...acc,
      [contactId]: {
        ...acc[contactId],
        [tagIdToAdd]: tagIdToAdd
      }
    };
  }, state.userTags);
};

但不幸的是,当有超过 20 万用户且每个用户大约有 5 个标签时,这会使浏览器崩溃。

我设法让它与这个一起工作:

const addTagToAllUsers = (state, tagIdToAdd) => {
  const stateUserTagsShallowCopy = {...state.userTags};
  const userIds = Object.keys(stateUserTags);
  for (let i = 0; i <= userIds.length - 1; i++) {
    const currUserId = userIds[i];
    stateUserTagsShallowCopy[currUserId] = {
      ...stateUserTagsShallowCopy[currUserId],
      [tagIdToAdd]: tagIdToAdd
    };
  }
  return stateUserTagsShallowCopy;
};

而且组件从商店更新得很好据我检查没有任何错误

但正如所写here:Redux 网站提到:

The key to updating nested data is that every level of nesting must be copied and updated appropriately

所以我想知道我的解决方案是否不好。

问题:

  1. 我相信我仍然很肤浅地应对状态中的所有级别,我错了吗?

  2. 这是一个糟糕的解决方案吗?如果是这样,它会产生哪些我可能会遗漏的错误?

  3. 为什么需要以不可变的方式更新子嵌套级别状态,如果存储选择器仍然会触发,因为父引用确实发生了变化。 (因为它适用于 顶层的浅层检查 。)

  4. 什么是最有效的解决方案?

关于问题3,这里有一个选择器代码的例子:

import {createFeatureSelector, createSelector, select} from '@ngrx/store';//ngrx version 10.0.0

//The reducer
const reducers = {
  userTags: (state, action) => {
    //the reducer function..
  }
}

//then on app module: 
StoreModule.forRoot(reducers)

//The selector :
const stateToUserTags = createSelector(
  createFeatureSelector('userTags'),
  (userTags) => {
    //this will execute whenever userTags state is updated, as long as it passes the shallow check comparison.
    //hence the question why is it required to return a new reference to every nested level object of the state...
    return userTags;
  }
)

//this.store is NGRX: Store<State>
const tags$: Observable<any> = this.store.pipe(select(stateToUser))


//then in component I use it something like this: 
<tagsList tags=[tags$ | async]>
</tagsList>

您的解决方案非常好。 根据经验,您不能改变存储在 state.

中的 object/array

在您的示例中,您唯一要改变的是 stateUserTagsShallowCopy 对象(它不存储在状态中,因为它是 state.userTags 的浅拷贝)。

旁注:最好在这里使用for of,因为您不需要访问索引

const addTagToAllUsers = (state, tagIdToAdd) => {
  const stateUserTagsShallowCopy = {...state.userTags};
  const userIds = Object.keys(stateUserTags);
  for (let currUserId of userIds) {
    stateUserTagsShallowCopy[currUserId] = {
      ...stateUserTagsShallowCopy[currUserId],
      [tagIdToAdd]: tagIdToAdd
    };
  }
  return stateUserTagsShallowCopy;
};

如果您决定使用 immer 这将如下所示

import produce from "immer";

const addTagToAllUsers = (state, tagIdToAdd) => {
  const updatedStateUserTags = produce(state.userTags, draft => {
    for (let draftTags of Object.values(draft)) {
       tags[tagIdToAdd] = tagIdToAdd
    }
  })
  return updatedStateUserTags
});

(这通常伴随着性能成本)。使用 immer,您可以牺牲性能来获得可读性

广告 3.

Why is it required to update sub nested level state in an immutable manner, if the store selector will still fire because the parent reference indeed changed. (Since it works with shallow checks on the top level.)

每次存储更改 select 或重新计算以查看依赖组件是否应该 re-render。

想象一下,我们决定改变用户内部的标签,而不是用户标签的不可变更新(state.userTags 是一个新的对象引用,但我们改变(重用)旧的条目对象 state.userTags[userId]

const addTagToAllUsers = (state, tagIdToAdd) => {
  const stateUserTagsShallowCopy = {...state.userTags};
  const userIds = Object.keys(stateUserTags);
  for (let currUserId of userIds) {
    stateUserTagsShallowCopy[currUserId][tagIdToAdd] = tagIdToAdd;
  }
  return stateUserTagsShallowCopy;
};

在你的例子中,你有一个 selector 取出 state.userTags。 这意味着每次发生状态更新时,nrgx 都会将 selector 的先前结果与当前结果(prevUserTags === currUserTags 通过引用)进行比较。在我们的例子中,我们更改 state.userTags 以便使用此 selector 的组件将使用新的 userTags.

刷新

但想象一下其他 select 将 select 只有一个用户标签而不是所有 userTags。在我们想象的情况下,我们直接变异 userTags[someUserId] 所以引用每次都保持不变。这里的负面影响是订阅组件不会刷新(添加标签后不会看到更新)。