Thabo
Thabo

Reputation: 1465

Angular universal: Route resolver + Firebase loading timeout

I am stuck with an issue and need assistance. I have a component that waits for some Firebase/Firestore data to be resolved by a router resolver like below.

    import { Injectable } from '@angular/core';
    import { AngularFirestore } from '@angular/fire/firestore';
    import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    
    @Injectable({ providedIn: 'root' })
    export class ArticleDataResolver implements Resolve<any> {
      constructor(
        private angularFirestore: AngularFirestore,
      ) { }
    
      resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): Observable<any> | Promise<any> | any {
        const stateKey = state.url;
        const ref = this.angularFirestore.collection("articles");
    
        return ref
          .doc(route.params.id)
          .get()
          .pipe(
            map((dataSnap) => {
              const articleData = dataSnap.data();
    
              return articleData;
            })
          );
      }
    }

In the consuming component, I have the subscription to the resolved observable data like below.

    this.activatedRoute.data.subscribe(
      (data) => {
        console.log(data);
      }
    );

This code works fine but stops working when a page is being refreshed or navigated to directly when being served with SSR using a cloud function as below.

    export const ssr = functions.runWith({
      timeoutSeconds: 300,
      memory: "1GB"
    }).https.onRequest((request, response) => {
      require(`${process.cwd()}/dist/motif/server`).app(request, response);
    });

But the same code works with my Node/Express server even on page refresh and direct navigation. For testing locally, I use the following script: npm run build:ssr && npm run serve:ssr

I am not sure what I am doing wrong because I do not get any errors but only cloud function timeout messages.

Upvotes: 1

Views: 711

Answers (2)

Thabo
Thabo

Reputation: 1465

Because the goal was to have the meta tags set dynamically for each article; and because the resolver was taking me for a ride; My solution is below without the resolver:

I have injected the state transfer service to my component constructor; which played a big rule in this solution

...// in the constructor
private state: TransferState
...

this.activatedRoute.params.subscribe((params) => {
  this.selectedArticleID = params.id;

  this.zone.runOutsideAngular(
    () => {
      let ARTICLE_KEY = makeStateKey(`${this.selectedArticleID}`)
      this.selectedArticle = this.state.get(ARTICLE_KEY, null);

      const ref = this.angularFirestore.collection("articles");

      if (!this.selectedArticle) {
        this.subscriptions.add(
          ref
            .doc(this.selectedArticleID)
            .get()
            .subscribe((dataSnap) => {
              this.templateString = "";

              // do somethings
              // update meta tags
            })
        );
      } else {
        // update meta tags using the server data from the state
      }
    }
  );
});

Upvotes: 0

Poul Kruijt
Poul Kruijt

Reputation: 71911

I don't know the exact answer, although I do know that a cloud function is not the appropriate way to serve a SSR application. Especially with a cold start you can have quite some lag in your page response, defeating the purpose of SSR.

However, as far as I know an Observable returned from a resolver needs to complete. You can find an entire discussion about this issue here. This would mean you need to add a take(1) to your pipe:

@Injectable({ providedIn: 'root' })
export class ArticleDataResolver implements Resolve<any> {
  constructor(
    private angularFirestore: AngularFirestore,
  ) { }

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<any> | Promise<any> | any {
    const stateKey = state.url;
    const ref = this.angularFirestore.collection("movementArticles");

    return ref
      .doc(route.params.id)
      .get()
      .pipe(
        map((dataSnap) => {
          const articleData = dataSnap.data();

          return articleData;
        }),
        take(1)
      );
  }
}

This however defeats the purpose of firestore as it doesn't update when you don't move back and forth from the route. You could do something like this:

@Injectable({ providedIn: 'root' })
export class ArticleDataResolver implements Resolve<any> {
  constructor(
    private angularFirestore: AngularFirestore,
  ) { }

  resolve(route: ActivatedRouteSnapshot): Observable<any> {
    const obs = this.angularFirestore.collection("movementArticles")
      .doc(route.params.id)
      .get()
      .pipe(
        map((snapshot) => snapshot.data(),
        shareReplay(1)
      );

    return obs.pipe(
      take(1),
      mapTo(obs)
    );
  }
}

Where you are actually setting an Observable as a result of the resolver, but only after it has returned once. I added a shareReplay(1). Be aware though that the firestore collection won't be unsubscribed to because of this. You could add a takeUntil statement which checks for route changes to know when to unsubscribe from it.

You would need to consume in your component like this by accessing the .snapshot which will contain the observable you used in the mapTo:

this.activatedRoute.snapshot.data.subscribe(
  (data) => {
    console.log(data);
  }
);

Upvotes: 2

Related Questions