angular / angularfire

Angular + Firebase = ❤️
https://firebaseopensource.com/projects/angular/angularfire2
MIT License
7.67k stars 2.19k forks source link

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

Open estatian opened 1 year ago

estatian commented 1 year ago

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: '

{{ data$ | async | json }}
' }) export class AppComponent implements OnInit {

data$: Observable | null = null;

constructor(private firestore: Firestore) { }

ngOnInit() {

const path = '/examples/GscrMmeBDasN6llxVybm'
const ref = doc(this.firestore, path);
this.data$ = docData(ref);

} }


8. npm run build:ssr && npm run serve:ssr
9. open http://localhost:4000 and view page source
10. refresh page source

<!-- detailed instructions to run your minimal repro or to recreate the environment -->

**Sample data and security rules**
N/A

<!-- include/attach/link to some json sample data (or provide credentials to a sanitized, test Firebase project) -->

### 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`.
hittten commented 1 year ago

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),
    )
  }
estatian commented 1 year ago

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?

itherocojp commented 1 year ago

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?

estatian commented 1 year ago

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.

tomscript commented 1 year ago

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

irvmar commented 1 year ago

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 {

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);
    }
  });
});

}

Benzilla commented 1 year ago

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);
  }
}
ndr commented 1 year ago

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,