Store: 馃悶[BUG]: Incorrect behavior when using @Selector

Created on 14 Aug 2018  路  5Comments  路  Source: ngxs/store

Versions

* ngxs: 3.1.4
* @angular/core: 6.0.0

Repro steps

Stackblitz / Github link: https://stackblitz.com/edit/ngxs-simple-selector-issue

  • Create a @Selector that uses map to change some state value.
  • Use this @selector with @Select in any component

Observed behavior

Select is triggering changes to the Selector created with map even though it does not change the value it uses.

image

Desired behavior

The correct behavior would be that it does not fire the changes unless it is changed in the state the value that it is using

docs

Most helpful comment

The others selector re-evaluates when the state changes but because it returns the same value as before the subscription does not fire. The observable has a distinctUntilChanged operator on it so the same returned value is ignored.

The Object.keys(...) call is returning a new value whenever the selector is re-evaluated which gets through the distinctUntilChanged. This is why it is different to the others selector.

Hope this answers all your questions ;-)

All 5 comments

Hi @abalad
This is how memoization works. The returned value is cached based on the same input value.
In this case modifying the counter property results in a change to the container state which is then a new value passed to the issueWithMap selector which in turn results in a new calculated result.

You can get the behavior you are expecting by breaking down the issueWithMap selector's dependency to only be the other prop. This can be done using a composite selector but unfortunately (in ngxs v3) any composite selectors defined within the state class automatically take a dependency on the container class. So, to get this working you would need to move this selector to an unrelated class (this will be fixed in v4 and not necessary) so that it only takes a dependency on the provided selector.

Here is an example evolved from yours in a stackblitz:
https://stackblitz.com/edit/ngxs-simple-selector-issue-yrfhnp

@markwhitfeld Thank you. What I was left in doubt is because the:

@Selector () static others (state: CounterStateModel) {
聽聽聽聽 return state.other;
}

Does not issue changes even though I change the state.counter.
Already when I realize a map() or any transformation in him it is shot.

@Selector() static issueWithMap(others: CounterStateModel) {
    return Object.keys(others)
}

That's what I do not understand.

The others selector re-evaluates when the state changes but because it returns the same value as before the subscription does not fire. The observable has a distinctUntilChanged operator on it so the same returned value is ignored.

The Object.keys(...) call is returning a new value whenever the selector is re-evaluated which gets through the distinctUntilChanged. This is why it is different to the others selector.

Hope this answers all your questions ;-)

@markwhitfeld Thank you so much. Now I understand.

To finalize the solution then final.

I've split my selectors into 2 files. One with the basic properties and the other file with all the selectors that the application will use.

usuario.state.ts

@State<UsuarioStateModel>({
  name: 'usuario',
  defaults: EntityStateModel.InitialState()
})
export class UsuarioState implements NgxsOnInit {

  @Selector()
  static selected(state: UsuarioStateModel) {
    return state.selected;
  }

  @Selector()
  static entities(state: UsuarioStateModel) {
    return state.entities;
  }

  constructor( private usuarioService: UsuarioService) {}

  @Action(LoadUsuarios)
  loadUsuario( ctx: StateContext<UsuarioStateModel> ) {
    return this.usuarioService.getAll().pipe(
      map( ( usuario: Usuario[] ) => ctx.dispatch( new LoadUsuariosSuccess( usuario['data']) ) ),
      catchError( ( error ) => ctx.dispatch( new OnError( error ) ) )
    );
  }

  @Action(LoadUsuariosSuccess)
  loadUsuarioSuccess( ctx: StateContext<UsuarioStateModel>, { payload }: LoadUsuariosSuccess ) {
    EntityAdapter.addAll( payload, ctx );
  }
.
.
.

usuario.selectors.ts

export class UsuarioSelectors {
  @Selector([UsuarioState.entities])
  static entities(entities: UsuarioStateModel['entities']) {
    return new EntitySelector().getEntities( entities );
  }

  @Selector([UsuarioState.selected])
  static selected(selected: UsuarioStateModel['selected']) {
    return selected;
  }
}

usuario.sandbox.ts

@Injectable()
export class UsuarioSandbox {

  @Select( UsuarioSelectors.selected ) usuarioSelected$: Observable<Usuario>;

  @Select( UsuarioSelectors.entities ) usuariosCollection$: Observable<Usuario[]>;

  constructor( private store: Store ) {}

  loadUsusuarios() {
    this.store.dispatch( new LoadUsuarios() );
  }

  addUsuario( usuario: Usuario ) {
    this.store.dispatch( new AddUsuario(usuario) );
  }
.
.
.
Was this page helpful?
0 / 5 - 0 ratings

Related issues

piernik picture piernik  路  5Comments

Newbie012 picture Newbie012  路  4Comments

un33k picture un33k  路  4Comments

am4apps picture am4apps  路  4Comments

garthmason picture garthmason  路  5Comments