DEV Community

Discussion on: NgRx Tips I Needed in the Beginning

Collapse
 
thardy profile image
Tim Hardy • Edited

In case you visit these comments again, let me spell out the issue with the code you show above and loading flags. I'm still curious how you actually deal with those in your real code.

  • let's assume the songs are already loaded, and the user navigates to the songsPage
  • you dispatch songsPageActions.opened when the songs page is opened
  • the reducer for songsPageActions.opened sets isLoading = true
  • any spinny loading gifs subscribed to isLoading start spinning - telling the user that songs are being loaded
  • an effect handling songsPageActions.opened gets stopped at the filter... filter(([, songs]) => !songs), and neither songsApiActions.songsLoadedSuccess nor songsApiActions.songsLoadedFailure gets dispatched, therefore isLoading never gets set to false. I'm assuming the reducers for both of the above actions are where you set isLoading = false
  • the loading spinny gifs continue to spin... forever... and ever... because they never know when the data is loaded

You cannot use the songsPageActions.opened to set isLoading = true because the effect that actually calls an api to load the songs CONDITIONALLY calls that api to load the songs. Without the same condition in your reducer (yuck) you don't know if songs are actually being loaded.

Once again, I like everything you evangelize in your post here, but I have no answer (and I don't see you presenting a working answer) to handling loading flags.

Collapse
 
markostanimirovic profile image
Marko Stanimirović This is Angular

Hi Tim!

You can do something like this:

export type LoadState = 'INIT' | 'LOADING' | 'LOADED' | { errorMsg: string };

// songs.reducer.ts

interface State {
  songs: Song[];
  loadState: LoadState;
}

const initialState: State = {
  songs: [],
  loadState: 'INIT',
};

export const reducer = createReducer(
  initialState,
  on(SongsPageActions.opened, (state) => state.loadState === 'LOADED'
    ? state
    : { ...state, loadState: 'LOADING' }
  ),
  on(SongsApiActions.songsLoadedSuccess, (state, { songs }) => ({
    ...state,
    songs,
    loadState: 'LOADED',
  }),
  on(SongsApiActions.songsLoadedFailure, (state, { errorMsg }) => ({
    ...state,
    loadState: { errorMsg },
  })
);

// songs-api.effects.ts

readonly loadSongsIfNotLoaded$ = createEffect(() => {
  return this.actions$.pipe(
    ofType(SongsPageActions.opened),
    concatLatestFrom(() => this.store.select(songsSelectors.selectLoadState)),
    filter(([, loadState]) => loadState !== 'LOADED'),
    exhaustMap(() => {
      return this.songsService.loadSongs().pipe(
        map((songs) => SongsApiActions.songsLoadedSuccess({ songs })),
        catchError((error) =>
          of(SongsApiActions.songsLoadedFailure({ errorMsg: error.message }))
        )
      );
    })
  );
});
Enter fullscreen mode Exit fullscreen mode

Additionaly, if you have a complex condition that is repeated in the effect as well as in the reducer, you can move it to a helper function and reuse it in both places.

Thread Thread
 
thardy profile image
Tim Hardy

Thanks a lot! I'm going to try this. I like the idea of making the condition something reusable. I also have come to the conclusion that I don't actually need these conditions as often as I thought. In reality, I rarely stop the loading of something because it's already loaded. The actions that cause something to load almost always cause them to load. On the rare occasion I really need a conditional load, this sounds like a clean way to handle it.