12

I was wondering what the best approach is to load data in Angular 5. Of course, we want to keep components dumb ;) For now, I am using a resolver and Angular calls the resolver in a specific route. Basically, data is loaded before a component has been initialised so I can show a loader animation to the user.

eg.

AResolver.resolver.ts

@Injectable()
export class AResolver implements Resolve<any> {
    constructor(private readonly aService: AService) {}

    resolve(route: ActivatedRouteSnapshot) {
        return this.aService.find(route.paramMap.get('id'));
    }
}

M.module.ts

export const MRoutes: Routes = [
    {
        path: 'route-x',
        component: AComponent,
        resolve: AResolver
    }
];

AComponent.component.ts

@Component({
    selector: 'a-super-fancy-name',
})
export class AComponent {

    constructor(private readonly route: ActivatedRoute) {}

}

Well, so far so good, but:

  1. What if I have resolvers with dependencies? So, to resolve B we need an id from A? Should I use a resolver wrapper? Thus I do something like:

Example

@Injectable()
export class ABResolver implements Resolve<any> {

    constructor(private readonly aService: AService, private readonly bService: BService) {}

    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        return this.aService.resolve(route, state).map((result) => this.bService.resolveWithId(result.Id));
    }
}

I guess this is the best approach because we still have 2 independent resolvers that can be reused. In general, is it a good practice to make resolver wrappers for all components that need data or only for resolvers that depend on other resolvers, or shouldn't I use resolver wrappers at all?

  1. What about other methods in a resolver (like the resolveWithId)?

  2. in AResolver route.paramMap is used the get the id. Isn't it better to pass the id because this looks tightly coupled?

  3. What are the alternatives?

  4. What are the drawbacks of the approach I use?

GuyT
  • 4,316
  • 2
  • 16
  • 30
  • Resolvers might be good with some improvement. But due to (IMO) huge limitations for now, I'm nearly never using them for now. It really feels like it's not playing well with streams and a reactive app. See more here https://stackoverflow.com/questions/49054232/why-would-you-use-a-resolver-with-angular I'm usually using ngrx and thus, I do manage all of the HTTP calls into effects. – maxime1992 Jul 02 '18 at 19:05

2 Answers2

0

Resolvers are good on paper, but only when you can easily get all params from current route, unfortunately this is rarely the case, sometimes you will have to access parent routes, sometimes you don't want to store all information in route parameters, sometimes you don't want to block navigation, also router params is not very flexible feature it could complicate refactoring easily (you can not simply change parent route params without affecting child resolvers and errors can not be checked by compiler).

There is no any predefined/recommended way in angular to load data. Depending on application scale you can either simply load data in each component as needed or impose certain rules or common interfaces/abstract classes or use caching shared services with rxjs etc. Basically you can only choose between custom architecture and Redux-like ngrx approach. ngrx obviously puts some restrictions and limitations as well as provides structure and general direction.

kemsky
  • 14,727
  • 3
  • 32
  • 51
  • Thank you for your answer, but I am looking for best practices and tradeoffs. I would like to hear common pitfalls and the reasons of why I should x instead of y :) – GuyT Jul 08 '18 at 10:05
0

Instead you can use InjectionTokens.

export const DI_ROUTE_ID = new InjectionToken<Observable<string>>('Route id$');

export function factoryRouteId(routeService: WhateverRouteParamObserver): Observable<string> {
  return routeService.params$.pipe(map((params) => params?.id ?? null));
}

export const DI_ROUTE_ID_PROVIDER: Provider = {
  provide: DI_ROUTE_ID,
  deps: [WhateverRouteParamObserver],
  useFactory: factoryRouteId,
};

Provide it in some module (maybe directly in app.module) i.e.:

@NgModule({
  providers: [
    ...
    DI_ROUTE_ID_PROVIDER,
  ],
})

And inject wherever needed.

export class DetailComponent {
  constructor(
    @Inject(DI_ROUTE_ID) private readonly id$: Observable<string>,
  ) {}
...

You can also create another DI token which depends on this ID token and on the http service and provides the Observable<WhateverDetailData> directly (and you can shareReplay it in the token directly).

export const DI_ROUTE_DETAILS = new InjectionToken<Observable<DetailsData>>('Route details$');

export function factoryRouteDetails(id$: Observable<string>, httpDataService: HttpDataService): Observable<DetailsData> {
  return id$.pipe(
    switchMap((id) => httpDataService.getData$(id).pipe(
      catchError(() => of(null))
    ),
    shareReplay({refCount: true, bufferSize: 1}),
  );
}

export const DI_ROUTE_DETAILS_PROVIDER: Provider = {
  provide: DI_ROUTE_DETAILS,
  deps: [DI_ROUTE_ID, HttpDataService],
  useFactory: factoryRouteDetails,
};
Alex Rempel
  • 480
  • 6
  • 12