1
votes

I need to access the state of a ngrx/data entity in a reducer function.

I'm building a pager (pagination) where the user can navigate to the last page among other options. But I don't know how many items there are in the cache, the EntitySelectorsFactory does (selectCount), but I'd not like to get the data in the component and use props to pass them, instead they should be in the reducer.

Or maybe there's a better way.

Actions

import {createAction, props} from '@ngrx/store';

export const toStart = createAction('[Pager] To Start');
export const toEnd = createAction('[Pager] To End');
export const incrementPage = createAction('[Pager] Increment');
export const decrementPage = createAction('[Pager] Decrement');
export const setPage = createAction('[Pager] Set', props<{page: number}>());
export const setPageSize = createAction('[Pager] Set Page Size', props<{size: number}>());

Reducers

import { Action, createReducer, on } from '@ngrx/store';
import * as PagerActions from '../actions/pager';
import {EntitySelectorsFactory} from '@ngrx/data';
import {Rant} from '../models/rant';

export const pagerFeatureKey = 'pager';

export interface State {
  page: number;
  pageSize: number;
}

export const initialState: State = {
  page: 1,
  pageSize: 10,
};

const rantSelectors = new EntitySelectorsFactory().create<Rant>('Rant');

const pagerReducer = createReducer(
  initialState,
  on(PagerActions.toStart, state => ({...state, page: 1})),
  on(PagerActions.toEnd, state => ({...state, page: Math.floor(rantSelectors.selectEntities.length / state.pageSize)})),
  on(PagerActions.setPage, (state, action) => ({...state, page: action.page})),
  on(PagerActions.incrementPage, state => ({...state, page: state.page + 1})), // TODO: check if out of bounds
  on(PagerActions.decrementPage, state => ({...state, page: state.page > 1 ? state.page - 1 : 1})),
  on(PagerActions.setPageSize, (state, action) => ({...state, pageSize: action.size}))
);

export function reducer(state: State | undefined, action: Action) {
  return pagerReducer(state, action);
}

No matter what I do in

  on(PagerActions.toEnd, state => ({...state, page: Math.floor(rantSelectors.selectEntities.length / state.pageSize)})),

it's always 0. rantSelectors.selectCount.length or the above. Probably because the length is 0. I'll also need to know the length or count of the "rant" entities in the increment reducer.

I don't know how to get the actual value.

I'm thinking... have a selector for the entity count and use props to pass that count in the toEnd action. Another option would be a side effect, but they're confusing and imho are not the proper way to write code (unreadable code, immediate cause/effect missing, yes I know, I don't want to argue about it). And then even if creating a side effect how would one define it in the module? Would it be an AppEffects side effect or a distinct side effect? I'm guessing AppEffects because it's not in another module.

app.module.ts

    EffectsModule.forRoot([AppEffects]),

AppEffects = pretty much empty, default ng add contents.

So yeah, I need to get the state of a @ngrx/data entity in the reducer, get its actual value, or a more elegant approach.

2
Fetch your length value before reducer, and pass this value with the action.Anarno
Yes. It's essentially the 1st approach. I've now got rid of toStart and toEnd actions and do the login in the component. It's not pretty but it works. I will move this functionality to a separate service.user12207064

2 Answers

1
votes

I prefer to keep my components clean... I see two solutions to your problem

  1. You could just store the length of rantSelectors.selectEntities.length in this part of the store, by adding a number value to your state. When you load/update the rantSelectors.selectEntities you then also update this slice of the store.
export interface State {
  page: number;
  pageSize: number;
  rantEntitiesSize: number;

// ...
   on(rantActions.loadSuccess, 
      rantActions.UpdateSuccess, //could include adding objects to the current loaded list
      rantActions.RemoveSuccess, //(negative number)
      (state, action) => 
        ({...state, rantEntitiesSize: (state.rantEntitiesSize + action)}),

}
  1. You could keep the toEnd action and not use that in the reducer, instead build an effect which is called on toEnd action that gets the values you need from the other part of the store and then calls a second action that passes the value to the reducer.
  toEndPager$ = createEffect(() =>
    this.actions$.pipe(
      ofType(PagerActions.toEnd),
      withLatestFrom(this.store.select(rantSelectors.selectEntities.length)),
      switchMap(([{},length]) => {
        return of(PagerActions.toEndSuccess({ length });         
      })
    )
  ); 
1
votes

It's an answer that doesn't answer the question directly. Instead I took a different approach, the 1st approach originally. It's not as elegant, but for re-usability I will put those functions into a separate service.

The functions being

  this.maxPages$ = this.store.select(selectMaximumRantPages);

  toStart() {
    this.store.dispatch(PagerActions.setPage({page: 1}));
  }
  toEnd() {
    this.maxPages$.subscribe(pages => {
      this.store.dispatch(PagerActions.setPage({page: pages}));
    }).unsubscribe();
  }
  setPage(page: number) {
    this.store.dispatch(PagerActions.setPage({page}));
  }
  setPageSize(size: number) {
    this.store.dispatch(PagerActions.setPageSize({size}));
  }
  increment() {
    this.store.dispatch(PagerActions.incrementPage());
  }
  decrement() {
    this.store.dispatch(PagerActions.decrementPage());
  }

One really only needs getters and setters. Convenience features it seems have no room in the ngrx world. I'll also get rid of increment and decrement actions and implement them as service methods.

selectors

export const selectPagerPage = (state: State) => state.pager.page;
export const selectPagerPageSize = (state: State) => state.pager.pageSize;
export const selectMaximumRantPages = createSelector(
  selectPagerPageSize,
  rantSelectors.selectCount,
  (size, count) => Math.ceil(count / size)
);