Angularfire: Route freezes with Resolver getting data from Firestore in SSR

Created on 26 Nov 2020  路  10Comments  路  Source: angular/angularfire

Version info

Angular: 11.0.2

Firebase: 8.1.1

AngularFire: 6.1.2

Other (e.g. Ionic/Cordova, Node, browser, operating system): Chrome 87, Windows 10, Angular Universal, Node 12.14.1

How to reproduce these conditions

  1. Create Angular Universal app with Firebase
  2. Add Resolver that fetches data from Firestore to a lazy route
  3. Try navigating to that route in SSR
    resolve({params}: ActivatedRouteSnapshot): Observable<any> {
        console.log('Resolver');

        return this.firestore
            .doc(params['id'])
            .get()
            .pipe(
                tap(() => console.log('Resolved')),
                catchError(() => this.router.navigate(['']) && EMPTY),
            );
    }

I also added console.log('Constructor') in that route's component constructor.

Debug output

Console logs print out correctly:

Resolver
Resolved
Constructor

Expected behavior

Route opens with resolved data

Actual behavior

Route is stuck on loading, server does not return the page

Here comes the weird part

There's a very strange way to kick-start it :)

1) Replace doc with collection in Resolver
2) Add some other request to Firestore in route's component (doesn't matter doc or collection)
3) Use async pipe in template to show that new request's result

I'm very confused.

It's hard to provide a reproduction, because it's SSR. I'll be happy to show it in my app if somebody is willing to live chat, it's pretty straightforward.

investigation bug

Most helpful comment

@Marcusg62, I was able to make your app serve the route from SSR - the issue is that you have several subscriptions and an angularfire auth subscription in your orderForm service, which is injected in your component. The auth subscription will never complete, and the other subscriptions don't either.

Instead of using the behavior subject in your service, refactor it so that there are no subscriptions in it, and pass your Restaurant to your initializeOrderObject() method. Only init the formGroup once and then use patchValue() on it in your initializeOrderObject method. Services have no lifecycle hooks so you can't control subscriptions.

Here's how you can make it serve the restaurant route (you will need to later inject the service again after you remove all the subscriptions in it):

component:

@Component({
  selector: 'app-restaurant',
  templateUrl: './restaurant.component.html',
  styleUrls: ['./restaurant.component.scss']
})
export class RestaurantComponent implements OnInit {
  restaurant: Observable<Restaurant>;

  constructor(
    private route: ActivatedRoute,
    public afs: AngularFirestore
  ) {}

  ngOnInit(): void {
    const path = this.route.snapshot.url[1].path;
    this.restaurant = this.afs.doc<Restaurant>('restaurants/' + path).valueChanges();
  }

}

template:

<div class="bg" *ngIf="(r | async) as r2">
  <!-- this just shows raw data, but it proves you can server-side render the route -->
  {{ r2 | json }}
</div>

All 10 comments

Here, I've managed to create a very basic repo:
https://github.com/waterplea/firebase-universal
Run npm i and then npm run dev:ssr.

Try loading this route in SSR (make sure it's SSR, open a new tab, paste address and hit Enter):
http://localhost:4200/issue/123
It would get stuck.

And this route has a hack of second request applied, it will not get stuck:
http://localhost:4200/hack/123

Code is very basic, I left a couple of comments. It was generated by plain old ng new + ng add @angular/fire + ng add @nguniversal/express-engine.

I have found one other hack - run Resolver outside of Angular with NgZone. It all seems to work fine for doc, doesn't help with collection. Both SSR and CSR work. Probably not a good idea for some reason, but I'll have to stick with it until the issue is fixed.

More news. Looks like my hack doesn't work once you build and deploy to Firebase :(
EDIT: False alarm, hack still works, my issue was different.

I am having the same issue. I have tried using promises as a workaround but no luck. Here is a link to my github repo with the bug. The route resolver is in src/app/resolvers/restaurant.resolver.ts

The problem is with ssr, so just npm i and npm run dev:ssr
Then copy paste this to your browser bar localhost:4200/restaurant/bistroKing

The behavior is exactly as @waterplea describes, you go to the specific route (to be sure it uses SSR), and the browser just spins with no timeout. I don't know why, but sometimes if you click into the bar again and hit enter while it was previously loading, it will then work without problem. See video:

https://user-images.githubusercontent.com/21998115/103108236-300df780-4603-11eb-920f-a11b83d2ab49.mov

@waterplea I tried running your app, was able to build and serve ssr, but there is a firestore permission issue:

[2021-01-05T16:24:54.497Z]  @firebase/firestore: Firestore (8.1.1): Could not reach Cloud Firestore backend. Connection failed 1 times. Most recent error: FirebaseError: [code=permission-denied]: Permission denied on resource project fir-universal-be375.

Also, there is no firebase ssr/universal function to serve the app. How are you hosting this?

@Marcusg62, I was able to make your app serve the route from SSR - the issue is that you have several subscriptions and an angularfire auth subscription in your orderForm service, which is injected in your component. The auth subscription will never complete, and the other subscriptions don't either.

Instead of using the behavior subject in your service, refactor it so that there are no subscriptions in it, and pass your Restaurant to your initializeOrderObject() method. Only init the formGroup once and then use patchValue() on it in your initializeOrderObject method. Services have no lifecycle hooks so you can't control subscriptions.

Here's how you can make it serve the restaurant route (you will need to later inject the service again after you remove all the subscriptions in it):

component:

@Component({
  selector: 'app-restaurant',
  templateUrl: './restaurant.component.html',
  styleUrls: ['./restaurant.component.scss']
})
export class RestaurantComponent implements OnInit {
  restaurant: Observable<Restaurant>;

  constructor(
    private route: ActivatedRoute,
    public afs: AngularFirestore
  ) {}

  ngOnInit(): void {
    const path = this.route.snapshot.url[1].path;
    this.restaurant = this.afs.doc<Restaurant>('restaurants/' + path).valueChanges();
  }

}

template:

<div class="bg" *ngIf="(r | async) as r2">
  <!-- this just shows raw data, but it proves you can server-side render the route -->
  {{ r2 | json }}
</div>

@inorganik I can't believe I didn't realize that the other subscriptions are the culprit. Thank so much!

@waterplea I tried running your app, was able to build and serve ssr, but there is a firestore permission issue:

[2021-01-05T16:24:54.497Z]  @firebase/firestore: Firestore (8.1.1): Could not reach Cloud Firestore backend. Connection failed 1 times. Most recent error: FirebaseError: [code=permission-denied]: Permission denied on resource project fir-universal-be375.

Also, there is no firebase ssr/universal function to serve the app. How are you hosting this?

I think the free demo ran out. Looks like a new firebase has to be created. And ssr function is created by default builder during ng deploy.

@waterplea Ah I didn't know it did that! Can you still test locally with firebase serve and have it hit the universal function?

I can confirm the same as @waterplea that when getting data fetched via resolver it hangs. I tried getting it multiple ways in the resolver, including an afs.doc() call and afs.collectionGroup() call. I reproduced in this repo which has different routes to test different ways of getting the same doc.

Was this page helpful?
0 / 5 - 0 ratings