Skip to content
This repository has been archived by the owner on Jan 10, 2018. It is now read-only.

Is NGRX Store compatible with Resolve to ensure data is loaded before a route is activated #270

Closed
jculverwell opened this issue Nov 12, 2016 · 14 comments

Comments

@jculverwell
Copy link

I recently read the article below regarding not activating routes until data is loaded

http://blog.thoughtram.io/angular/2016/10/10/resolving-route-data-in-angular-2.html

Is this approach compatible with NGRX store? Does anyone have an example of how we might achieve this?

@orizens
Copy link

orizens commented Nov 15, 2016

"ngrx/store" shouldn't be necessarily used in a "resolve" function.
however, inside the "resolve" function, you can use "take(1)" from the "store" to retrieve a snapshot of the store, but you'll have to return some kind of a promise.

@jjstreet
Copy link

jjstreet commented Nov 15, 2016

i actually worked it the other way. i have a resolver that will dispatch an action with the resolved data that does nothing but put the data into the store. you could even have it dispatch the success action that the reducer listens to put data into the store's state.

@jculverwell
Copy link
Author

Thanks guys, appreciate your feedback. I also found the guard in the the example app useful https://github.com/ngrx/example-app/blob/master/src/app/guards/book-exists.ts . I could use the same sort of approach with resolve.

 /**
   * This method creates an observable that waits for the `loaded` property
   * of the collection state to turn `true`, emitting one time once loading
   * has finished.
   */
  waitForCollectionToLoad(): Observable<boolean> {
    return this.store.let(fromRoot.getCollectionLoaded)
      .filter(loaded => loaded)
      .take(1);
  }

/**
   * This is the actual method the router will call when our guard is run.
   *
   * Our guard waits for the collection to load, then it checks if we need
   * to request a book from the API or if we already have it in our cache.
   * If it finds it in the cache or in the API, it returns an Observable
   * of `true` and the route is rendered successfully.
   *
   * If it was unable to find it in our cache or in the API, this guard
   * will return an Observable of `false`, causing the router to move
   * on to the next candidate route. In this case, it will move on
   * to the 404 page.
   */
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
    return this.waitForCollectionToLoad()
      .switchMap(() => this.hasBook(route.params['id']));
  }

@MikeRyanDev
Copy link
Member

Seems like this has been resolved. Please reopen and leave a comment if it is not.

@colthreepv
Copy link

I am trying another way, would like to hear your thoughts about it.

@Injectable()
export class SomethingResolver implements Resolve<Action> {

  constructor (
    private store: Store<State>,
    private action$: Actions
  ) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Action> {
    this.store.dispatch({ type: Actions.FETCH_SOMETHING });
    return this.action$.ofType(Actions.RETRIEVE_SOMETHING)
      .take(1)
      .toPromise();
  }
}

Then using it in the resolve Object like:

export const routes: Routes = [
  {
    path: '',
    component: SomethingComponent,
    resolve: { something: SomethingResolver }
  }
]

Basically this resolver dispatch a "request" action, and expects a "retrieve" action before unblocking the angular/router. (the Effects and the reducers will handle data fetching and data storing)

In this way, the SomethingComponent will just access the data using it's store, not having to rely upon ActivatedRoute.data.subscribe or reading the Store data manually (you need a selector for that!)

@marcincichocki
Copy link

@colthreepv Why are you returning Promise? resolve can return observable too.

@yuristsepaniuk
Copy link

yuristsepaniuk commented Jun 26, 2017

@colthreepv Hello colthreepv! Sorry, I am not really aware of full context, but I wish to share my thoughts with you. Probably too late :) With resolve your page should wait and then navigate :( If I am not wrong. With your design this will be a challenge to achieve the following ux --> 1. we see empty form with spinner. 2. bumm, we have response from the server. 3. we see spinner goes away and we see rerendered form without spinner.

I really like this effect from ux perspective. With resolver it wont be possible to navigate first and show empty form. Let me know what do you think, my friend?

-Yura

@e-cloud
Copy link

e-cloud commented Jul 6, 2017

@yuristsepaniuk It seems a little weird that a form plays a route component. I don't resolve should do that kind of work.

I bet you would want this https://www.bennadel.com/blog/3139-experimenting-with-conditional-enter-leave-animations-in-angular-2-rc-6.htm.

@yuristsepaniuk
Copy link

yuristsepaniuk commented Jul 10, 2017

@e-cloud Hey! I think we are on the same page. Form doesn't play routing, it is router with no 'resolve' on it, which is responsible for navigation. Form is responsible for triggering action to get data from the server, not resolver on the router before navigation. If we are stuck with resolving before navigation (on Router), you can't see empty form with spinner (and no animation make sense here) and it is bad UX practice. We should see empty form, then back end follows with response, then we see re-rendered data.

Thx

@Silthus
Copy link

Silthus commented Jul 13, 2017

@colthreepv how would you handle an error response in this case?

The solution only handles successful loading, but in the example of the angular hero app, we would want to navigate back to a different route if loading failed.

@endyjasmi
Copy link

endyjasmi commented Jul 23, 2017

@Silthus Inspired by @colthreepv's example. Currently I am using this in my application. I am not quite sure if this will have memory leak issues. Anyone knows if race will unsubscibe both observable?

@Injectable()
export class SomethingResolver implements Resolve<Action> {

  constructor (
    private store: Store<State>,
    private action$: Actions
  ) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Action> {
    this.store.dispatch({ type: Actions.FETCH_SOMETHING });
    const responseOK = this.action$.ofType(Actions.RESPONSE_OK);
    const responseError = this.action$.ofType(Actions.RESPONSE_ERROR)
      .do(() => this.router.navigate(['']);
    return Observable.race(responseOk, responseError).take(1);
  }
}

@e-cloud
Copy link

e-cloud commented Jul 23, 2017

@endyjasmi race will not do any unsubscription, and you should check rxjs docs.

@matze1234
Copy link

Can somebody else comment on @endyjasmi's solution? It looks very interesting to me, because it also considers a failed request. In all other solutions i've seen so far, it seems that the guard filters for a successful load event, and waits forever if it has failed. So does the ngrx-example app.

@rpm911
Copy link

rpm911 commented Aug 7, 2017

After examining the RxJS race tests here spec-js/observables/race-spec.js and here spec-js/operators/take-spec.js I have concluded that race will properly unsubscribe the responseOK and responseError observables when take(1) completes.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests