NgRx effect that conditionally loads data - how to handle loading flag?

Viewed 61

I've moved from imperative actions to purely past-tense actions, and I really like how it moves app intelligence out of my components and into my Effects. I used to use something like...

this.store.dispatch(MemberActions.loadMembers());

...in my MemberListComponent, but with a filter on whether the data was currently loading or already loaded, etc.

Now, I'm trying the following in my component...

this.store.dispatch(MemberActions.memberListInitialized());

with the following effect...

getAllMembers$ = createEffect(
() => this.actions$
  .pipe(
    ofType(MemberActions.memberListInitialized),
    concatLatestFrom(() => this.store.select(MemberSelectors.isLoaded)),
    filter(([action, loaded]) => !loaded),
    switchMap(action => {
      return this.memberService.getAll();
    }),
    map((members) => MemberActions.membersLoaded({members}))
  )
);

I really like the indirection here, how it moves the intelligence, especially the logic to conditionally load the members, out of the component and into the effect. The only difficulty is I can't think of an elegant/clean way to handle the loading flag on the state for this data. I really don't want to add a reducer for memberListInitialized that also has logic to figure out if I'm actually loading members. I can easily set loading to false in a reducer for membersLoaded, but when do I set loading to true?

Here's the state for this module...

export const initialState: MemberState = {
  members: [],
  loading: false,
  loaded: false,
};
1 Answers

Three Actions:

1 for Page, 2 for API

You should have Success and Fail actions for your effect, and then you can set loading to false in the reducers for those actions.

Here's a complete example of what it could look like:

Actions:

export const loadMembers = createAction('[Members Page] Load');
export const loadMembersSuccess = createAction('[MembersAPI] Load Success', props<{ members: Member[] }>());
export const loadMembersFail = createAction('[MembersAPI] Load Fail', props<{ error: string }>());

Effect:

effectName$ = createEffect(() => {
    return this.actions$.pipe(
        ofType(MemberActions.loadMembers),
        mergeMap(() => this.memberService.getMembers().pipe(
            map(members=> MemberActions.loadMembersSuccess({ members })),
            catchError(error => of(MemberActions.loadMembers()Fail({ error })))
        )));
});

Reducers:

export interface MemberState {
  members: Member[]
  loading: boolean
  error: string
}
const initialState: MemberState = {
  members: [],
  loading: false,
  error: ''
}

export const memberReducer = createReducer<MemberState>(
  initialState,
  on(MemberActions.loadMembers, (state): MemberState => {
    return {
      ...state,
      loading: true
    }
  }),
  on(MemberActions.loadMembersSuccess, (state, action): MemberState => 
  {
    return {
      ...state,
      members: action.members,
      loading: false,
      error: ''
    }
  }),
  on(MemberActions.loadMembersFail, (state, action): MemberState => {
    return {
      ...state,
      members: [],
      loading:false,
      error: action.error
    }
  })
)

Selectors:

export interface State extends AppState.State {
    members: MemberState
}

const getMemberFeatureState = createFeatureSelector<MemberState>('members')
export const getMembers = createSelector(getMemberFeatureState, state => state.member)
export const getMembersLoading = createSelector(getMemberFeatureState, state => state.loading)

In your constructor:

this.members$ = this.store.select(getMembers)
this.loading$ = this.store.select(getMembersLoading)
this.store.dispatch(MemberActions.loadMembers())

ngrx-data

Another option to avoid all this boiler plate is to use ngrx-data. The loading flag is already present out of the box for you to use in its implementation.

Related