Store: Problem with NgxsStoragePluginModule and NgxsReduxDevtoolsPluginModule together

Created on 27 Mar 2018  路  19Comments  路  Source: ngxs/store

Versions

* ngxs: 2.0.0-rc.24
* @ngxs/devtools-plugin: 2.0.0-rc.24
* @ngxs/storage-plugin: 2.0.0-rc.24
  • Configure my plugins:
 NgxsReduxDevtoolsPluginModule.forRoot({
      disabled: false
    }),
    NgxsStoragePluginModule.forRoot({
      key: '@@STATE',
      storage: StorageOption.LocalStorage,
      deserialize: JSON.parse,
      serialize: JSON.stringify
    })
  • Dispacht a accion that belongs to de NgxsModule.forRoot([AppState])
@Action(SearchPeoples)
  serarchPeoples({getState, setState}: StateContext<AppStateModel>) {
    const state = getState();
    return this.http.get<Search>('https://randomuser.me/api/?results=5').pipe(
      tap(value => {
        setState({
          ...state,
          search: value.results
        });
      })
    );
  }
search() {
    this.store.dispatch(new SearchPeoples()).subscribe(value => {
      console.log('The action is completed.');
    });
  }
  • Navigate to the feature module (lazy loading)

in the console show the error:

captura

because in the feature module, show a component that need featureSearch :

export class FooComponent {

  @Select(state => state.featureStage.featureSearch)
  result$: Observable<Result>;

  constructor(private store: Store) {
  }

  search() {
    this.store.dispatch(new SearchPeoplesFeature({countPeople: 8, test: true})).subscribe(value => {
      console.log('The action is completed.');
    });
  }

}

If I delete the plugin @ngxs/storage-plugin this does not happen.

I leave a gif with the steps of the problem:
bug

core

Most helpful comment

Both are correct. It's just that the Select() is trying to retrieve state.featureStage.featureSearch before featureStage is initialized. What you'd want to do is initialize it or give it a default value before theSelect() tries to grab it.

When you set up the state, you can provide the defaults. Something like:

@State({
  name: 'featureStage',
  defaults: {
    featureSearch: {}
  }
})

All 19 comments

I think this might have been fixed by @deebloo just this morning.

Sounds like to me that the the @Select() is trying to access state.featureStage before it actually exists (undefined) thus the error. In the GIF you're navigating before you initialize the featureStage and getting the error. Only after you click the button to populate the featureStage does it work properly. Unless I'm missing something, this doesn't seem like an issue with ngxs or any of the plugins.

You might be able to work around this by creating a selector in your state. I'm assuming that your state is called FeatureState

Something like:

@State({
  // ...
})
export class FeatureState {
  @Selector()
  static featureSearch(state: any) {
    return state && state.featureSearch;
  }

  // other actions ...
}

And then change your select to be like:

@Select(FeatureState.featureSearch)
result$: Observable<Result>;

@yarrgh You're right, in this way the error is not given, but...

when it is correct to use: @Select(state => state.featureStage.featureSearch)
and when to use: @Select(FeatureState.featureSearch)

I wish I could use it in both ways, in any circumstances

Both are correct. It's just that the Select() is trying to retrieve state.featureStage.featureSearch before featureStage is initialized. What you'd want to do is initialize it or give it a default value before theSelect() tries to grab it.

When you set up the state, you can provide the defaults. Something like:

@State({
  name: 'featureStage',
  defaults: {
    featureSearch: {}
  }
})

@amcdnl Is that my state already has a default value, which is an empty array:

export interface FeatureStateModel {
  featureSearch: Result[];
}

@State<FeatureStateModel>({
  name: 'featureStage',
  defaults: {
    featureSearch: []
  }
})

and I think it sure happens because I'm loading a lazy module, and it seems this:

@Component({
  selector: 'app-foo',
  template: `
    <p>Foo Component</p>
    <button (click)="search()">Search peoples and put feature state</button>
    <span>
  <ul>
    <li *ngFor="let res of result$ | async">
      {{res.name.first | json}}
    </li>
  </ul>
</span>
  `
})
export class FooComponent {

  @Select(state => state.featureStage.featureSearch)
  result$: Observable<Result>;

  constructor(private store: Store) {
  }

it runs first this:

 defaults: {
    featureSearch: []
  }

My conclusion is i can not use this: @Select(state => state.featureStage.featureSearch) for lazy modules

If you want to select using a lambda and you want to select something that doesn't exist, you will have to do null checks. until typescript supports optional chaining (https://tc39.github.io/proposal-optional-chaining/) state?.authState?.isLoggedIn then we will have to check the paths all the way down state && state.authState && state.authState.isLoggedIn

As to if you should use the lambda or selector i think the selector is better because you can specify the intent of the query, since they may become quite advanced. It's why you break a class into multiple methods, you want to be able to reason about a bit of code by just understanding the method name.

  • AuthState.getLoggedInUser instead of state => state.auth != null
  • ModalState.isModalOpen instead of state => state.layout.modal.isOpen

Issue
So I don't think this is a bug, but a problem with how we structure our apps.
We can absolutely try to write some good documentation on the matter, and find some good patterns to follow :)

I agree I don't think this is a bug

so I just have to be careful when using @Select(state => state.featureStage.featureSearch) in lazy modules?

@mailok Yes, but not just there but everywhere. It's more or less a race condition. That select is trying to access the state when it hasn't yet been initialized.

I'm curious, does it work if you change it this way?

export class FooComponent {

  result$: Observable<Result>;

  constructor(private store: Store) {
  }

  ngOnInit() {
    this.result$ = this.store.select(state => state.featureStage.featureSearch);
  }

}

@yarrgh No, it does not work, but this works very well @Select('featureStage.featureSearch')

I'm going to introduce a try catch block around the selector invokation, which will swallow the TypeError that gets thrown if the selector lambda tries to access a variable on the state that doesn't exist

What's interesting is that I'm not able to duplicate this: https://stackblitz.com/edit/ngxs-simple-amvqoh

@mailok Am I missing anything that would cause the error?

@yarrgh I see that I did not charge the lazy module from the beginning, I do it through a button

I've just committed a fix for the problem with TypeError's in a select.

It's available in version: 2.0.0-dev.cf0bffa

What I'm trying to duplicate is the error you're getting when featureStage is undefined. I tried to duplicate this by creating nested objects in the AuthState and have the defaults initialize the state.

I've updated my stackblitz to do navigation on a button click. (so that lazy loading kicks in). I'm attempting to get the Select in AuthComponent to load before the store has been initialized.

I had same race condition issue with enum type in state, even if I initialized defaults
I am using safe navigation workaround
https://github.com/xmlking/nx-starter-kit/blob/master/libs/auth/src/auth.state.ts#L47

Can we consider this closed now?

I think I was able to duplicate this. I had to clear localStorage, dispatch an action (click the 'click me' button) so that the state gets persisted in localStorage, then navigate to the auth route. I believe this is caused by the property not existing in localStorage at the time it is read from the state.

I believe #180 is tracking that issue.

Thanks! Closing since we are tracking that.

Was this page helpful?
0 / 5 - 0 ratings