【问题标题】:NgRx Select Errors When Attempting Access on Nested Properties尝试访问嵌套属性时 NgRx 选择错误
【发布时间】:2020-07-10 08:21:41
【问题描述】:

在访问嵌套属性时使用 NgRx 选择函数时出现 TypeErrors。

我在app.module.ts 中配置了我的根存储,如下所示:

StoreModule.forRoot({ app: appReducer }),

app reducer 只是一个标准的 reducer。它正确地设置了状态;我可以在 redux 开发工具中看到这一点。一些出错的嵌套属性的选择器是:

const getAppFeatureState = createFeatureSelector<IAppState>('app');

export const getAppConfig = createSelector(getAppFeatureState, state => {
  return state.appConfig.data;
});

export const getConfigControls = createSelector(getAppConfig, state => {
  console.log({ state }) // logs values from initial state
  return state.controls;
});

export const getConfigDropdowns = createSelector(
  getConfigControls,
  state => state.dropdowns,
);

当我像这样在app.compontent.ts 订阅这些选择器时

ngOnInit() {
  this.store.dispatch(new appActions.LoadAppConfig());
  this.store
    .pipe(select(appSelectors.getConfigDropdowns))
    .subscribe(data => {
      console.log('OnInit Dropdowns Data: ', data);
    });
}

app.component.ts:31 ERROR TypeError: Cannot read property 'dropdowns' of null at app.selectors.ts:18

当我将日志记录添加到链更高的选择器时,我可以看到唯一记录的元素是 initialState 值,它们设置为 null。我不认为这个选择器函数应该在值从它的初始值改变之前触发。但既然它没有,我得到这个错误也就不足为奇了,因为它试图访问 null 上的属性。 initialState 是否有必要包含所有潜在的未来嵌套属性的完整树,以免破坏我的选择器?

如何防止此选择器在其值不变时触发?

另外,StoreModule.forRoot 的配置是否正确?让我有些困惑的是,创建一个“根”存储,在我的 redux 存储中创建与我的模块存储平行的 app 键,即模块存储不在 app 之下。

编辑:

添加app.reducer.ts的通用结构。我使用immer 来缩短更新嵌套属性所需的样板文件,但是我也尝试过这个reducer 作为更传统的类型,它在整个地方都有传播运算符,它的工作原理是一样的。

import produce from 'immer';

export const appReducer = produce(
  (
    draftState: rootStateModels.IAppState = initialState,
    action: AppActions,
  ) => {
    switch (action.type) {
      case AppActionTypes.LoadAppConfig: {
        draftState.appConfig.meta.isLoading = true;
        break;
      }
      /* more cases updating the properties accessed in problematic selectors */
      default: {
        return draftState; // I think this default block is unnecessary based on immer documentation
      }
    }
}

编辑:添加initialState

const initialState: rootStateModels.IAppState = {
  user: null,
  appConfig: {
    meta: {isError: false, isLoading: false, isSuccess: false},
    data: {
      controls: {
        dropdowns: null,
      }
    },
  },
};

【问题讨论】:

  • 我认为问题在于您如何注册功能状态。您正在将状态注册为 root,但将其作为功能状态访问。将您的州注册为StoreModule.forFeature - 参考 - ngrx.io/guide/store/reducers#register-feature-state
  • @user2216584 我正在查看他们的forRoot documentation,我所拥有的似乎是正确的。我使用 forFeature 语法来注册单独的模块。
  • 1) 请提供您的initialState 值。 2) 你不应该在你的 reducer 中改变 draftState,而是返回一个新的 {...draftState, ...} 值。 3) 建议:存储中的深层嵌套对象可能很难管理。
  • @ThierryFalvo 我正在完全按照 immer 文档直接更新 draftState。有什么问题?
  • @ThierryFalvo 我已经添加了initialState

标签: javascript angular typescript rxjs ngrx


【解决方案1】:

因为你更新了你的问题,所以答案是https://www.learnrxjs.io/learn-rxjs/operators/filtering/distinctuntilchanged

它只允许在值被更改时发出值。

store.pipe(
  map(state => state.feature.something),
  distinctUntilChanged(),
)

要求 state.feautre.something 已更改。

正确的方法是使用createSelector 函数,该函数返回与distinctUntilChanged 工作方式相同的记忆选择器。

【讨论】:

  • 嗯。我不这么认为。我已将initialState 作为默认值添加到reducer 的状态参数中。我已经更新了相关代码。
  • ngrx.io/guide/store/reducers#creating-the-reducer-function 后面有减速器功能吗? export const appReducer = produce(... 还不够。来自这部分Note: The exported reducer function is necessary as function calls are not supported by the AOT compiler.
  • 我不关注。我使用记忆的createSelector函数。
  • 好的,现在我看到你改变了问题。我的问题是——你想达到什么目标?因为一开始它是关于一个错误,现在是关于防止选择器。您上面的代码可以正常工作。如果您想从商店中选择一次 - 将 take(1) 添加到管道 .pipe(select(getConfigDropdowns), take(1))
  • 改变了我的问题——你在做什么?我已经添加了一些代码,这是我所做的唯一编辑。错误发生在选择器中,我想阻止它。我已经多次阅读了您的答案和 cmets。我不认为我们在同一页上。避免这些选择器错误应该是可以实现的,而不需要管道一堆额外的 RxJS 操作符
【解决方案2】:

好的,我再次查看代码并更新了我的答案。

你可以试试下面给出的示例吗?

this.store
  .pipe(
    // Here `isStarted` will be boolean value which will enable and disable selector. 
    //This can be derived from initial state, if null it wont go to next selector
    switchMap(data => {
      if (isStarted) {
        return never();
      } else {
        return of(data);
      }
    }),
    switchMap(data => select(appSelectors.getConfigDropdowns))
  )
  .subscribe(data => {
    console.log("OnInit Dropdowns Data: ", data);
  });

【讨论】:

  • 你不返回draftState,你只是改变它。
  • 是的,但需要从减速器返回状态。同一时间丢失.. mutate 不推荐。不可预知的行为
  • We'll explore how to perform side effects in the advanced walkthrough. For now, just remember that the reducer must be pure. Given the same arguments, it should calculate the next state and return it. No surprises. No side effects. No API calls. No mutations. Just a calculation. redux.js.org/basics/reducers
  • 我已经监督了你的整个问题。我已经更新了我的答案。请立即检查。
【解决方案3】:

dispatch 方法是异步的。 所以:

ngOnInit() {
  this.store.dispatch(new appActions.LoadAppConfig());
  this.store
    .pipe(select(appSelectors.getConfigDropdowns))
    .subscribe(data => {
      console.log('OnInit Dropdowns Data: ', data);
    });
}

这里订阅的运行速度比dispatch 快,因此选择从您的初始状态返回空值。只需在选择器中检查它或添加初始状态。例如:

const getAppFeatureState = createFeatureSelector<IAppState>('app');

export const getAppConfig = createSelector(getAppFeatureState, state => {
  return state.appConfig.data;
});

export const getConfigControls = createSelector(getAppConfig, state => {
  console.log({ state }) // logs values from initial state
  return state.controls;
});

export const getConfigDropdowns = createSelector(
  getConfigControls,
  state => state ? state.dropdown : null,
);

【讨论】:

  • 如果这些选择器之一提前返回了 false/falsy,它不会触发订阅吗?我的意思是,我觉得您仍然需要像pipe(filter(Boolean)) 之类的过滤器,不是吗?
【解决方案4】:

您可以使用filter 运算符确保它只为有效值发出值,然后您可以使用pluck 运算符发出相应嵌套属性的值。

store.pipe(
  filter(value => state.feature.something),
  pluck('feature', 'something'),
)

【讨论】:

    猜你喜欢
    • 2022-01-23
    • 2021-05-21
    • 1970-01-01
    • 2012-03-18
    • 1970-01-01
    • 1970-01-01
    • 2012-08-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多