Search code examples
angularngrxngrx-storengrx-store-8.0ngrx-data

ngrx 8: Access state of @ngrx/data entity from reducer function


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.


Solution

  • 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 });         
          })
        )
      );