angularrxjsrxjs-observablesangular-resolver

Angular resolvers that return observables, only wait for the first value


Before all my components are rendered, my app needs some basic data that has to be available to all components.
Therefore I wrote a resolver that returns an observable.

export const appRoutes: Route[] = [
   {
      path: '',
      resolve: { data: appWideRootResolver },
      children: [
         {
            path: '',
            pathMatch: 'full',
            redirectTo: 'dashboard',
         },
         //further routes...
      ]
    }
}

To test how my resolver works, I started with an easy example:

export const appWideRootResolver: ResolveFn<number> = (_) => {
   const a$ = timer(4000, 1000).pipe(
      tap((x) => {
         console.log(x);
      })
   );
   return a$;
}

The result seems strange to me:
I would have expected that in this example, my components will never be rendered, because my observable never completes.

However, Angular waits only for a single emission, stops the subscription and resolves the resolver.

Is this by design? Or am I doing something wrong?

For me it would make perfect sense, if an resolver returns an observable, to wait for it to complete instead of just being fine with the first emission.


Solution

  • Looking at the source code of resolve_data.ts, you can see a first rxjs operator, so it might only consider the first emission.

    Also it does not make sense for a resolver to wait for future emissions, since it's critical for loading the component to the end user.

    function resolveNode(
      resolve: ResolveData,
      futureARS: ActivatedRouteSnapshot,
      futureRSS: RouterStateSnapshot,
      injector: EnvironmentInjector,
    ): Observable<any> {
      const keys = getDataKeys(resolve);
      if (keys.length === 0) {
        return of({});
      }
      const data: {[k: string | symbol]: any} = {};
      return from(keys).pipe(
        mergeMap((key) =>
          getResolver(resolve[key], futureARS, futureRSS, injector).pipe(
            first(), // <- notice here!
            tap((value: any) => {
              if (value instanceof RedirectCommand) {
                throw redirectingNavigationError(new DefaultUrlSerializer(), value);
              }
              data[key] = value;
            }),
          ),
        ),
        takeLast(1),
        mapTo(data),
        catchError((e: unknown) => (isEmptyError(e as Error) ? EMPTY : throwError(e))),
      );
    }