38

I have a static set of data, a list of countries, that are used on some components. This data is loaded upon the ngOnInit() of these components but I'd like to load them only if it's the very first time that I request the data (the store is empty). Every subsequent times I load the component I'd like to simply use the data from the store without "refreshing" it.

How is this achievable using ngrx?

I'm using Effects. This is how my code looks like:

The component:

export class EditPageComponent implements OnInit {
countries$: Observable<Country[]>

constructor(private store: Store<fromContacts.State>) {
    this.countries$ = store.select(fromContacts.getCountriesEntities);
}
ngOnInit() {
   this.store.dispatch(new countries.Load());
}

The Effect:

    @Effect()
      loadCollection$: Observable<Action> = this.actions$
        .ofType(countries.LOAD)
        .switchMap(() =>
          this.countriesService
        .getCountries()
        .map((countriesList: Country[]) => {
          return new countries.LoadSuccess(countriesList);
        })
        .catch(error => of(new countries.LoadFail(error)))
  );

And the reducer:

case countries.LOAD_SUCCESS: {

      const countriesList: Country[] = action.payload;
      const reducedCountries: { [id: string]: Country } = countriesList.reduce((countrs: { [id: string]: Country }, countr: Country) => {
        return Object.assign(countrs, {
            [countr.code]: countr
        });
    }, {});

Thanks, Gab

gabric
  • 1,865
  • 2
  • 21
  • 32
  • Are you using `Effects`? Can you add the code where you load the data? (not the service itself but the call to it) – maxime1992 Oct 12 '17 at 14:40
  • 1
    Can you simple make the call to the server in the "ngOnInit()" handler of the project's root component instead of in each component that needs the data? – Jim Oct 13 '17 at 20:21
  • @Jim Nice one, I hadn't thought about it. Works like a charm – gabric Oct 14 '17 at 19:18
  • Do we have an working example for this or any code in github? – Developer Jul 19 '23 at 12:17

5 Answers5

42

There are different ways of doing this. First of all you can keep a hasLoaded: boolean property in the state. Then you can check this before you make the service get call.

ngOnInit() {
  this.store.select(getHasLoaded)
    .take(1)
    .subscribe(hasLoaded => {
      if (!hasLoaded) this.store.dispatch(new countries.Load()); 
    }
}

Another option is to let your @Effect check the hasLoaded property:

@Effect()
  loadCollection$: Observable<Action> = this.actions$
    .ofType(countries.LOAD)
    .withLatestFrom(this.store.select(getHasLoaded)
    .filter(([ action, hasLoaded ]) => !hasLoaded) // only continue if hasLoaded is false 
    .switchMap(() =>
      this.countriesService
        .getCountries()
        .map((countriesList: Country[]) => {
          return new countries.LoadSuccess(countriesList);
        })
    .catch(error => of(new countries.LoadFail(error)))
);

For this to work you need to provide the store in your Effects constructor.

Saber
  • 5,150
  • 4
  • 31
  • 43
Hetty de Vries
  • 441
  • 4
  • 5
  • 1
    This is fine if you are only loading one dataset from one endpoint. But once your app starts getting large, you really do not want the hassle of having multiple booleans to manage. There are better alternatives as shown in other answers. Also I recommend this article to understand why this is bad practice: https://medium.com/@m3po22/stop-using-ngrx-effects-for-that-a6ccfe186399 – rmcsharry Sep 28 '19 at 10:25
  • The first solution is not complete. you need to first dispatch the action then subscribe to and store the result. In your solution are just dispatching an action. – Saber Feb 26 '20 at 01:05
  • You could try this lib: https://github.com/amzhang/ngrx-loading-state It's boilerplatey, but sits entirely on top of NgRx. At least it's a consistent way to deal with conditionally issuing API calls. – flux Jul 29 '22 at 12:18
14

TL;DR

Use take(1) operator in the effect

Don't forget the error handling using catchError and return EMPTY, otherwise when an error occurs, that error will be returned always (timeout, auth error, offline...)


I had exactly the same case as you, what I did was adding in the effects the rxjs operator take to fetch the countries only the first time the LoadCountries action was dispatched.

@Effect()
  loadCountries$: Observable<CoreActions> = this.actions$.pipe(
    ofType(CoreActionTypes.LoadCountries),
    mergeMap(() =>
      this.countriesService.getAllCountries().pipe(
        map(c => new LoadCountriesSuccess(c)),
        catchError(() => {
          this.store.dispatch(new LoadCountriesFailed());
          return EMPTY;
        })
      )
    ),
    take(1)
  );

Returning EMPTY inside catchError will complete the observable without passing through the take(1)

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
  • 2
    Wouldn't it make more sense to return the `LoadCountriesFailed`-action (eg. `return of(new LoadCountriesFailed())`, than dispatch it, and return an empty observable stream? – Anders Jan 16 '20 at 09:00
  • The `take(1)` solution is a neat trick but it only works if you don’t do SSR with state transferred and hydrated on the client. Then the `take` operator will let the action fire again on the client even though it’s already been fired on the server and the data is in the store. – Martin Sotirov Mar 31 '21 at 08:41
13

You can select the countries from the store within effects, if they are represented in the store we ignore the action, if not we fetch the countries.

@Effect()
getOrder = this.actions.pipe(
  ofType<GetOrder>(ActionTypes.GetOrder),
  withLatestFrom(this.store.pipe(select(getOrders))),
  filter(([{payload}, orders]) => !!orders[payload.orderId])
  mergeMap([{payload}] => {
    ...
  })
)

For more info see Start using ngrx/effects for this.

timdeschryver
  • 14,415
  • 1
  • 19
  • 32
1

Use withLatestFrom in the effect

also, check || products.length === 1, this case will happen when the user lands on a single product page.

  loadProducts$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(loadProducts),
      withLatestFrom(this.store.select(getProducts)),
      mergeMap(([action, products]) => {
        if (!products.length || products.length === 1) {
          ...
          return this.productsService.getProducts().pipe(
            map((products) => {
              ...
              return loadProductsSuccess({ products });
            }),
            catchError((error) => {
              ...
              return of(errorAction());
            })
          );
        }
        return of(dummyAction());
      })
    );
  });

Mansour Alnasser
  • 4,446
  • 5
  • 40
  • 51
0

My answer is a variation from Hetty de Vries answer's. If you simply want to retrieve the contents of the store without touching the effects you could do something similar to this:

this.store.select(state => state.producer.id).forEach( id => {
...
}

supposing that your store contains a producer object with an attribute id.

Investigator
  • 1,431
  • 2
  • 17
  • 24