angularfire icon indicating copy to clipboard operation
angularfire copied to clipboard

Document from Firestore only appears on first render with Angular Universal (SSR)

Open estatian opened this issue 2 years ago • 8 comments

Version info

Angular: 15.2.0

Firebase: 9.8.0 (or whatever AngularFire pulls in...)

AngularFire: 7.5.0

Other (e.g. Ionic/Cordova, Node, browser, operating system): Node: 16.19.1 Browser: Chromium 111.0.5563.64 OS: Pop!_OS Linux

How to reproduce these conditions

Failing test unit, Stackblitz demonstrating the problem (follow steps below)

Steps to set up and reproduce

  1. Create a Firestore collection (e.g. 'examples') with a document containing some data.
  2. ng new ssr-test --routing=true --style=scss
  3. cd ssr-test
  4. ng generate environments
  5. ng add @nguniversal/express-engine
  6. ng add @angular/fire (hook it up with Firestore from step 1)
  7. Replace app.component.ts with the following (path in ngOnInit refers to step 1):
import { Component, OnInit } from '@angular/core';
import { doc, docData, Firestore } from '@angular/fire/firestore';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: '<pre>{{ data$ | async | json }}</pre>'
})
export class AppComponent implements OnInit {

  data$: Observable<any> | null = null;

  constructor(private firestore: Firestore) {
  }

  ngOnInit() {

    const path = '/examples/GscrMmeBDasN6llxVybm'
    const ref = doc(this.firestore, path);
    this.data$ = docData(ref);
  }
}
  1. npm run build:ssr && npm run serve:ssr
  2. open http://localhost:4000 and view page source
  3. refresh page source

Sample data and security rules N/A

Debug output

** Errors in the JavaScript console ** N/A

** Output from firebase.database().enableLogging(true); ** N/A

** Screenshots ** N/A

Expected behavior

Content of document from Firestore should appear on first view of source page and on every subsequent refresh.

Actual behavior

Content of document from Firestore appears on first view of source page, then on subsequent refreshes is appears as null.

estatian avatar Mar 12 '23 10:03 estatian

I have also tried this way

constructor(
    private fs: Firestore,
    private state: TransferState,
  ) {
    const key = makeStateKey<unknown>('FIRESTORE')
    const existing = state.get(key, undefined)
    const ref = collection(this.fs, "names")

    this.$items = collectionData(ref).pipe(
      traceUntilFirst('firestore'),
      tap(it => state.set(key, it)),
      existing ? startWith(existing) : tap(),
      tap(console.log),
    )
  }

hittten avatar May 02 '23 19:05 hittten

Two months and no response... anyone know if the approach described even should work?

The description at docs/universal/getting-started.md is 5 years old and doesn't quite seem to apply anymore.

Is there an updated tutorial somewhere?

estatian avatar May 11 '23 12:05 estatian

How did you create the data? On my firestore emulator suite, adding documents without creating any collection did not work.

But this first render issue sometimes occurs in other situation for me. Anyone knows?

itherocojp avatar May 11 '23 13:05 itherocojp

I just added a collection in Firebase called examples and a document which happened to be auto-ID'ed GscrMmeBDasN6llxVybm. Could and should be anything, as far as I can understand.

Just threw in some example data like name = Example object

Stuff like that.

estatian avatar May 12 '23 20:05 estatian

Can you create a simple repo using stackblitz or something. I had an issue a while back with collectionData which I later migrated away from in favor of vanilla Firestore (no angular fire)

If u make a repo I can take a look

tomscript avatar May 12 '23 20:05 tomscript

Maybe it's related to my exact problem. I use TransferState and it just transfers the state the first render then on subsequent refreshes there is no data in the client. This is how I retrieve data:

public async getLink(userNameToCheck: string): Promise<Link> {

return new Promise<Link>(async (resolve, reject) => {
  this.zone.runOutsideAngular(() => {
    try {
      const observable = this.afs.collection("links")
        .doc(userNameToCheck + environment.secretKey)
        .get()
        .pipe(
          map((doc) => {
            if (doc.exists) {
              return doc.data() as Link;
            } else {
              console.error("No such document!");
              return null;
            }
          }),
          take(1)
        );
      observable.subscribe(link => {
        this.transferState.set<Link>(this.linkStateKey, link);
        resolve(link);
      });
    } catch (error) {
      reject(error);
    }
  });
});

}

irvmar avatar May 30 '23 07:05 irvmar

Also had the same problem:

First compile & reload of the page the TransferState worked, but every subsequent reload failed. This only happened if I was making a call to Firestore. I think it has something to do with Firebase / AngularFire making the Angular app "Unstable".

After trying and failing to debug for 12 hours or so, I'm now using the Firebase Firestore REST API for TransferState on my initial page load. You'll have to do some parsing of this data afterwards as its in a bit of a different format.

Hope this helps someone!

import { Component } from '@angular/core';
import { SsrService } from "../../services/ssr.service";
import { makeStateKey, TransferState } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError, firstValueFrom } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Component({
  selector: 'app-hydration',
  templateUrl: './hydration.component.html',
  styleUrls: ['./hydration.component.css']
})
export class HydrationComponent {

  postData: any;

  constructor(
    private http: HttpClient,
    private ssr : SsrService,
    private transferState: TransferState
    )
  {}

  async ngOnInit(): Promise<void> {
    if(!this.ssr.isBrowser){
      this.postData = await firstValueFrom(this.FetchData("my-post-slug"));
      this.SaveState('posts', this.postData);
    }
    else{
      if(this.HasState('posts')){
        this.postData = this.GetState('posts');
        console.log(this.postData);
      }
      else{
        console.log("No data");
      }
    }
  }

  FetchData(slug: string): Observable<any> {
    const query = {
      structuredQuery: {
        where: {
          fieldFilter : { 
            field: { fieldPath: "slug" }, 
            op: "EQUAL", 
            value: { stringValue: slug } 
          }
        },
        from: [{ collectionId: "Posts" }]
      }
    };

    const url = 'https://firestore.googleapis.com/v1/projects/[YOUR PROJECT NAME]/databases/(default)/documents:runQuery';

    return this.http.post(url, query)
      .pipe(
        catchError(error => {
          console.error('Error:', error);
          return throwError(error);
        })
      );
  }

  SaveState<T>(key: string, data: any): void {
    this.transferState.set<T>(makeStateKey(key), data);
  }

  GetState<T>(key: string, defaultValue: any = null): T {
    const state = this.transferState.get<T>(
      makeStateKey(key),
      defaultValue
    );
    return state;
  }

  HasState<T>(key: string) {
    return this.transferState.hasKey<T>(makeStateKey(key));
  }
}

SsrService

import { Injectable } from '@angular/core';
import { Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({
  providedIn: 'root'
})
export class SsrService {

  isBrowser : boolean;

  constructor(@Inject(PLATFORM_ID) platformId: Object,) {
    this.isBrowser = isPlatformBrowser(platformId);
  }
}

Benzilla avatar Jun 16 '23 12:06 Benzilla

The exact same problem as described:

The minimum example:

import { DocumentReference, Firestore, doc, docData } from "@angular/fire/firestore";

import { Observable } from "rxjs";
import { tap } from "rxjs/operators";


@Injectable({
    providedIn: 'root',
  })
  export class FirebaseProxyService {
    constructor(
        private firestore: Firestore,
      ) {
      }

      public getDocRef(path: string, id?: string): DocumentReference {
        if (id) {
          return doc(this.firestore, path, id);
        }
        return doc(this.firestore, path);
      }

      public doc(collectionPath: string, id: string): Observable<any> {
        console.log('this.doc', collectionPath, id);
        return docData(this.getDocRef(collectionPath, id))
          .pipe(
            tap((docData) => {
              console.log('docData', docData);
            }),
          );
      }

  }

The method that I am using is "doc", which is evaluating, returns data and console.logs (in tap operator) it only once after ssr restart (it is returns all docData which app needs, but only in first render)

Versions: angular: 15.2 @angular/fire: 7.6.1,

ndr avatar Aug 08 '23 20:08 ndr