How do you get the user db details related to an authUser in Firestore?

I understand from the last line of your question (users = () => this.db.collection('users');) that the collection where you store extra info on the users is called users and that a user document in this collection uses the userId (uid) as docId.

The following should do the trick (untested):

class Firebase {
  constructor() {
    app.initializeApp(config).firestore();
    /* helpers */
    this.fieldValue = app.firestore.FieldValue;


    /* Firebase APIs */
    this.auth = app.auth();
    this.db = app.firestore();

onAuthUserListener = (next, fallback) =>
    this.auth.onAuthStateChanged(authUser => {
      if (authUser) {
           this.db.collection('users').doc(authUser.uid)
              .get()
              .then(snapshot => {
                const userData = snapshot.data();
                console.log(userData);
                //Do whatever you need with userData
                //i.e. merging it with authUser
                //......

                next(authUser);
          });
      } else {
        fallback();
      }
    });

So, within the observer set through the onAuthStateChanged() method, when we detect that the user is signed in (i.e. in if (authUser) {}), we use its uid to query the unique document corresponding to this user in the users collection (see read one document, and the doc of the get() method).


I have a theory that I'd like you to test.

I think that when you are calling next(authUser) inside your onAuthStateChanged handler, during it's execution it encounters an error (such as cannot read property 'name' of undefined at ...).

The reason your code is not working as expected is because where you call next(authUser), it is inside the then() of a Promise chain. Any errors thrown inside a Promise will be caught and cause the Promise to be rejected. When a Promise is rejected, it will call it's attached error handlers with the error. The Promise chain in question currently doesn't have any such error handler.

If I've lost you, have a read of this blog post for a Promises crash course and then come back.

So what can we do to avoid such a situation? The simplest would be to call next(authUser) outside of the Promise then() handler's scope. We can do this using window.setTimeout(function).

So in your code, you would replace

next(authUser)

with

setTimeout(() => next(authUser))
// or setTimeout(() => next(authUser), 0) for the same result

This will throw any errors as normal rather than being caught by the Promise chain.

Importantly, you haven't got a catch handler that handles when userDocRef.get() fails. So just add .catch(() => setTimeout(fallback)) on the end of then() so that your code uses the fallback method if it errors out.

So we end up with:

this.user(authUser.uid)
  .get()
  .then(snapshot => {
    const dbUser = snapshot.data();
    // default empty roles
    if (!dbUser.roles) {
      dbUser.roles = {};
    }
    // merge auth and db user
    authUser = {
      ...dbUser, // CHANGED: Moved dbUser to beginning so it doesn't override other info
      uid: authUser.uid,
      email: authUser.email,
      emailVerified: authUser.emailVerified,
      providerData: authUser.providerData
    };
    setTimeout(() => next(authUser), 0); // invoke callback outside of Promise
  })
  .catch((err) => setTimeout(() => fallback(), 0)); // invoke callback outside of Promise

Edited code

The above explanation should allow you to fix your code but here is my version of your Firebase class with various ease-of-use changes.

Usage:

import FirebaseHelper from './FirebaseHelper.js';

const fb = new FirebaseHelper();
fb.onUserDataListener(userData => {
  // do something - user is logged in!
}, () => {
  // do something - user isn't logged in or an error occurred
}

Class definition:

// granular Firebase namespace import
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';

const config = { /* firebase config goes here */ };

export default class FirebaseHelper { // renamed from `Firebase` to prevent confusion
  constructor() {
    /* init SDK if needed */
    if (firebase.apps.length == 0) { firebase.initializeApp(config); }

    /* helpers */
    this.fieldValue = app.firestore.FieldValue;

    /* Firebase APIs */
    this.auth = firebase.auth();
    this.db = firebase.firestore();
  }

  getUserDocRef(uid) { // renamed from `user`
    return this.db.doc(`users/${uid}`);
  }

  getUsersColRef() { // renamed from `users`
    return this.db.collection('users');
  }

  /**
   * Attaches listeners to user information events.
   * @param {function} next - event callback that receives user data objects
   * @param {function} fallback - event callback that is called on errors or when user not logged in
   *
   * @returns {function} unsubscribe function for this listener
   */
  onUserDataListener(next, fallback) {
    return this.auth.onAuthStateChanged(authUser => {
      if (!authUser) {
        // user not logged in, call fallback handler
        fallback();
        return;
      }

      this.getUserDocRef(authUser.uid).get()
        .then(snapshot => {
          let snapshotData = snapshot.data();

          let userData = {
            ...snapshotData, // snapshotData first so it doesn't override information from authUser object
            uid: authUser.uid,
            email: authUser.email,
            emailVerified: authUser.emailVerifed,
            providerData: authUser.providerData
          };

          setTimeout(() => next(userData), 0); // escapes this Promise's error handler
        })
        .catch(err => {
          // TODO: Handle error?
          console.error('Error while getting user document -> ', err.code ? err.code + ': ' + err.message : (err.message || err));
          setTimeout(fallback, 0); // escapes this Promise's error handler
        });
    });
  }

  // ... other methods ...
}

Note that in this version, the onUserDataListener method returns the unsubscribe function from onAuthStateChanged. When your component is unmounted, you should detach any relevant listeners so you don't get a memory leak or have broken code running in the background when it's not needed.

class SomeComponent {
  constructor() {
    this._unsubscribe = fb.onUserDataListener(userData => {
      // do something - user is logged in!
    }, () => {
      // do something - user isn't logged in or an error occurred
    };
  }

  // later
  componentWillUnmount() {
    this._unsubscribe();
  }
}