10

I am trying to figure out how to get a user name which is an attribute stored in a user collection, which has been merged with the attributes created by the firebase authentication model.

I can access authUser - which gives me the limited fields firebase collects in the authentication tool, and then I'm trying to get from there to the related user collection (which uses the same uid).

I have a react context consumer with:

import React from 'react';
const AuthUserContext = React.createContext(null);
export default AuthUserContext;

Then in my component I'm trying to use:

const Test = () => (

<AuthUserContext.Consumer>
    {authUser => (

    <div>
            {authUser.email} // I can access the attributes in the authentication collection 
            {authUser.uid.user.name} //i cannot find a way to get the details in the related user collection document - where the uid on the collection is the same as the uid on the authentication table


     </div>
    )}
</AuthUserContext.Consumer>
);

const condition = authUser => !!authUser;
export default compose(
withEmailVerification,
withAuthorization(condition),
)(Test);

In my firebase.js - I think I've tried to merge the authUser attributes from the Authentication model with the user collection attributes as follows:

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.user(authUser.uid)
          .get()
          .then(snapshot => {
            const dbUser = snapshot.data();
            // default empty roles
            if (!dbUser.roles) {
              dbUser.roles = {};
            }
            // merge auth and db user
            authUser = {
              uid: authUser.uid,
              email: authUser.email,
              emailVerified: authUser.emailVerified,
              providerData: authUser.providerData,
              ...dbUser,
            };
            next(authUser);
          });
      } else {
        fallback();
      }
    });

I can't find a way to get from the authUser (which works to get me to the Authentication attributes) - to the user collection which has an id with the same uid from the Authentication collection.

I have seen this post, which seems to have the same problem and tried to work out what the answer is supposed to be hinting at - but I can't seem to find a way that works to get from the Authentication collection to the user collection and I don't know what merge is doing for me if it isn't giving me access to the attributes on the user collection from authUser.

I tried to use a helper in my firebase.js to give me a user from a uid - but that doesn't seem to help either.

user = uid => this.db.doc(`users/${uid}`);
  users = () => this.db.collection('users');

Next attempt

To add more background, I have made a test component that can log (but not render) authUser, as follows:

import React, { Component } from 'react';
import { withFirebase } from '../Firebase/Index';
import { Button, Layout  } from 'antd';

import { AuthUserContext, withAuthorization, withEmailVerification } from '../Session/Index';


class Test extends Component {
  constructor(props) {
    super(props);

    this.state = {
      loading: false,
      user: null,
      ...props.location.state,
    };
  }

  componentDidMount() {
    if (this.state.user) {
      return;
    }

    this.setState({ loading: true });

    // this.unsubscribe = this.props.firebase
    //   .user(authUser.uid)
    //   .onSnapshot(snapshot => {
    //     const userData = snapshot.data();  
    //     console.log(userData);
    //     this.setState({
    //       user: snapshot.data(),
    //       loading: false,
    //     });
    //   });
  }

  componentWillUnmount() {
    this.unsubscribe && this.unsubscribe();
  }



  render() {
    const { user, loading } = this.state;


    return (
        <div>
        <AuthUserContext.Consumer>
        {authUser => (
            console.log(authUser),
            <p>
                </p>


            )}
            </AuthUserContext.Consumer> 

        </div>

    );

    };
}
export default Test;

The log shows the details for uid, email etc in the logs, but it's in amongst a long list of items - many of which are prefaced with 1 or 2 letters (I can't find a key to find out what each of these prefix letters mean). Example extracted below:

enter image description here

UPDATE ON THIS COMMENT:

Previously, I said: Fields for uid, email etc do not appear to be nested beneath these prefixes, but if I try to:

 console.log(authUser.email)

, I get an error that says:

TypeError: Cannot read property 'email' of null

The update: I just realised that in the console log, I have to expand a drop down menu that is labelled:

Q {I: Array(0), l:

to see the email attribute. Does anyone know what this jibberish alludes to? I can't find a key to figure out what Q,I or l means, to know if I'm supposed to be referencing these things to get to the relevant attributes in the Authentication table. Maybe if I can figure that out - I can find a way to get to the user collection using the uid from the Authentication collection.

Has anyone used react on the front end, with a context consumer to find out who the current user is? If so, how do you access their attributes on the Authentication model and how did you access the attributes on the related User collection (where the docId on the User document is the uid from the Authentication table)?

NEXT ATTEMPT

Next attempt has produced a very strange outcome.

I have 2 separate pages that are context consumers. The difference between them is that one is a function and the other is a class component.

In the function component, I can render {authUser.email}. When I try to do the same thing in the class component, I get an error that says:

TypeError: Cannot read property 'email' of null

This error is coming from the same session with the same logged in user.

Note: while the firebase documentation says that a currentUser property is available on auth - I have not been able to get that to work at all.

My function component has:

import React from 'react';
import { Link } from 'react-router-dom';
import { compose } from 'recompose';
import { AuthUserContext, withAuthorization, withEmailVerification } from '../Session/Index';


const Account = () => (

<AuthUserContext.Consumer>
    {authUser => (
    <div>
         {authUser.email}
    </div>
    )}
</AuthUserContext.Consumer>
);

// const condition = authUser => !!authUser;
// export default compose(
// withEmailVerification,
// withAuthorization(condition),
// )(Account);
export default Account;

While I can't get to the User collection attributes where the docId on the user document is the same as the uid of the authenticated user, from this component, I can output the email attribute on the auth collection for this user.

While the Firebase documentation provides this advice for managing users and accessing attributes here, I have not found a way to implement this approach in react. Every variation of an an attempt at doing this, both by making helpers in my firebase.js and by trying to start from scratch in a component produces errors in accessing firebase. I can however produce a list of users and their related User collection information (I can't get a user based on who the authUser is).

My class component has:

import React from 'react';
import {
    BrowserRouter as Router,
    Route,
    Link,
    Switch,

  } from 'react-router-dom';
import * as ROUTES from '../../constants/Routes';
import { compose } from 'recompose';
import { withFirebase } from '../Firebase/Index';
import { AuthUserContext, withAuthorization, withEmailVerification } from '../Session/Index';



class Dashboard extends React.Component {
  state = {
    collapsed: false,
  };

  onCollapse = collapsed => {
    console.log(collapsed);
    this.setState({ collapsed });
  };

  render() {
    const {  loading } = this.state;
    // const dbUser = this.props.firebase.app.snapshot.data();
    // const user = Firebase.auth().currentUser;
    return (
    <AuthUserContext.Consumer>
      {authUser => (  

        <div>    
         {authUser.email} // error message as shown above
          {console.log(authUser)} // output logged in amongst a long list of menus prefixed with either 1 or 2 characters. I can't find a key to decipher what these menus mean or do.
        </div>
      )}
    </AuthUserContext.Consumer>  
    );
  }
}

//export default withFirebase(Dashboard);
export default Dashboard;

In my AuthContext.Provider - I have:

import React from 'react';
import { AuthUserContext } from '../Session/Index';
import { withFirebase } from '../Firebase/Index';
const withAuthentication = Component => {
  class WithAuthentication extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        authUser: null,
      };  
    }

    componentDidMount() {
      this.listener = this.props.firebase.auth.onAuthStateChanged(
        authUser => {
          authUser
            ? this.setState({ authUser })
            : this.setState({ authUser: null });
        },
      );
    }

    componentWillUnmount() {
      this.listener();
    };  

    render() {
      return (
        <AuthUserContext.Provider value={this.state.authUser}>
          <Component {...this.props} />
        </AuthUserContext.Provider>
      );
    }
  }
  return withFirebase(WithAuthentication);

};
export default withAuthentication;

NEXT ATTEMPT

It is really strange that with this attempt, I'm trying to console log the values that I can see exist in the database and the value of name is being returned as 'undefined' where the db has a string in it.

This attempt has:

    import React from 'react';
    import {
        BrowserRouter as Router,
        Route,
        Link,
        Switch,
        useRouteMatch,
     } from 'react-router-dom';
    import * as ROUTES from '../../constants/Routes';
    import { compose } from 'recompose';
    import { withFirebase } from '../Firebase/Index';
    import { AuthUserContext, withAuthorization, withEmailVerification } from '../Session/Index';



    class Dash extends React.Component {
      // state = {
      //   collapsed: false,
      // };

      constructor(props) {
        super(props);

        this.state = {
          collapsed: false,
          loading: false,
          user: null,
          ...props.location.state,
        };
      }
      componentDidMount() {
        if (this.state.user) {
          return;
        }

        this.setState({ loading: true });

        this.unsubscribe = this.props.firebase
          .user(this.props.match.params.id)
          // .user(this.props.user.uid)
          // .user(authUser.uid)
          // .user(authUser.id)
          // .user(Firebase.auth().currentUser.id)
          // .user(Firebase.auth().currentUser.uid)

          .onSnapshot(snapshot => {
            this.setState({
              user: snapshot.data(),
              loading: false,
            });
          });
      }

      componentWillUnmount() {
        this.unsubscribe && this.unsubscribe();
      }


      onCollapse = collapsed => {
        console.log(collapsed);
        this.setState({ collapsed });
      };

      render() {
        // const {  loading } = this.state;
        const { user, loading } = this.state;
        // let match = useRouteMatch();
        // const dbUser = this.props.firebase.app.snapshot.data();
        // const user = Firebase.auth().currentUser;
        return (
        <AuthUserContext.Consumer>
          {authUser => (  

            <div>    
            {loading && <div>Loading ...</div>}

                <Layout style={{ minHeight: '100vh' }}>
                  <Sider collapsible collapsed={this.state.collapsed} onCollapse={this.onCollapse}>
                    <div  />

                  </Sider>
                <Layout>

                    <Header>
                    {console.log("authUser:", authUser)}
                    // this log returns the big long list of outputs - the screen shot posted above is an extract. It includes the correct Authentication table (collection) attributes
                    {console.log("authUser uid:", authUser.uid)}
                    // this log returns the correct uid of the current logged in user
                    {console.log("Current User:", this.props.firebase.auth.currentUser.uid)}
// this log returns the correct uid of the current logged in user
                    {console.log("current user:", this.props.firebase.db.collection("users").doc(this.props.firebase.auth.currentUser.uid
                    ))}
// this log returns a big long list of things under a heading: DocumentReference {_key: DocumentKey, firestore: Firestore, _firestoreClient: FirestoreClient}. One of the attributes is: id: (...) (I can't click to expand this).
                    {console.log("current user:", this.props.firebase.db.collection("users").doc(this.props.firebase.auth.currentUser.uid
                    ).name)}
//this log returns: undefined. There is an attribute in my user document called 'name'. It has a string value on the document with the id which is the same as the currentUser.uid.
                    <Text style={{ float: 'right', color: "#fff"}}>

                      {user && (
                        <Text style={{ color: "#fff"}}>{user.name}
//this just gets skipped over in the output. No error but also does not return the name.
</Text>


                      )}

                    </Text>
                    </Header>      
                   </Layout>
                </Layout>

            </div>
          )}
        </AuthUserContext.Consumer>  
        );
      }
    }

    export default withFirebase(Dash);

NEXT ATTEMPT

So this attempt is clumsy and doesn't make use of the helpers or the snapshot queries that I tried to use above, but log the user collection document attributes to the console as follows:

{ this.props.firebase.db.collection('users').doc(authUser.uid).get()

      .then(doc => {
          console.log(doc.data().name) 
      })

    } 

What I can't do though is find a way to render that name in the jsx

How do you actually print the output?

When I try:

{ 


this.props.firebase.db.collection('users').doc(authUser.uid).get().data().name

                }

I get an error that says:

TypeError: this.props.firebase.db.collection(...).doc(...).get(...).data is not a function

When I try:

{ 



this.props.firebase.db.collection('users').doc(authUser.uid).get()
              .then(doc => {
                  console.log(doc.data().name), 
                  <p>doc.data().name</p>
              })
            } 

I get an error that says:

Line 281:23: Expected an assignment or function call and instead saw an expression no-unused-expressions

When I try:

{ 


this.props.firebase.db.collection('users').doc(authUser.uid).get("name")
              .then(doc => {
                  console.log(doc.data().name), 
                  <p>doc.data().name</p>
              })
            }

The error message says:

Expected an assignment or function call and instead saw an expression

I'm ready to give up on trying to find out how to get the snapshot queries to work - if I can just get the name of the user collection to render on the screen. Can anyone help with that step?

NEXT ATTEMPT

I found this post. It has a good explanation of what needs to happen, but I can't implement it as shown because componentDidMount does not know what authUser is.

My current attempt is as follows - however, as currently written, authUser is a wrapper on the return value - and the componentDidMount segment does not know what authUser is.

import React from 'react';
import {
    BrowserRouter as Router,
    Route,
    Link,
    Switch,
    useRouteMatch,
 } from 'react-router-dom';
import * as ROUTES from '../../constants/Routes';
import { compose } from 'recompose';
import { Divider, Layout, Card, Tabs, Typography, Menu, Breadcrumb, Icon } from 'antd';
import { withFirebase } from '../Firebase/Index';
import { AuthUserContext, withAuthorization, withEmailVerification } from '../Session/Index';




const { Title, Text } = Typography
const { TabPane } = Tabs;
const { Header, Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;


class Dashboard extends React.Component {
  // state = {
  //   collapsed: false,
  //   loading: false,
  // };

  constructor(props) {
    super(props);

    this.state = {
      collapsed: false,
      loading: false,
      user: null,
      ...props.location.state,
    };
  }
  componentDidMount() {
    if (this.state.user) {
      return;
    }

    this.setState({ loading: true });

    this.unsubscribe = this.props.firebase
      .user(this.props.match.params.id)
      .onSnapshot(snapshot => {
        this.setState({
          user: snapshot.data(),
          loading: false,
        });
      });
  // }

//   firebase.firestore().collection("users")
//     .doc(this.state.uid)
//     .get()
//     .then(doc => {
//       this.setState({ post_user_name: doc.data().name });
//   });
// }

  this.props.firebase.db
    .collection('users')
    .doc(authUser.uid)
    .get()
    .then(doc => {
        this.setState({ user_name: doc.data().name });
        // loading: false,
      });  
    }                  

  componentWillUnmount() {
    this.unsubscribe && this.unsubscribe();
  }


  onCollapse = collapsed => {
    console.log(collapsed);
    this.setState({ collapsed });
  };

  render() {
    // const {  loading } = this.state;
    // const { user, loading } = this.state;
    // let match = useRouteMatch();
    // const dbUser = this.props.firebase.app.snapshot.data();
    // const user = Firebase.auth().currentUser;


    return (
    <AuthUserContext.Consumer>
      { authUser => (  

        <div>    

                <Header>

                 {/* 
                    { 
                    this.props.firebase.db.collection('users').doc(authUser.uid).get()
                    .then(doc => {
                        console.log( doc.data().name
)                          
                    })
                  } 
                  */} 


                  </Text>
                </Header>      

                      <Switch>

                      </Switch>    

        </div>
      )}
    </AuthUserContext.Consumer>  
    );
  }
}

export default withFirebase(Dashboard);

NEXT ATTEMPT

Next up, i tried wrapping the route for dashboard inside the AuthContext.Consumer so that the whole component could use it - thereby letting me access the logged in user in the componentDidMount function.

I changed the route to:

<Route path={ROUTES.DASHBOARD} render={props => (
          <AuthUserContext.Consumer>
             { authUser => ( 
                <Dashboard authUser={authUser} {...props} />  
             )}
          </AuthUserContext.Consumer>
        )} />

and removed the consumer from the dashboard component render statement.

Then in the componentDidMount on the Dashboard component, i tried:

componentDidMount() {
    if (this.state.user) {
      return;
    }

    this.setState({ loading: true });

     this.unsubscribe =
     this.props.firebase.db
     .collection('users')
   //.doc(this.props.firebase.db.collection('users').doc(this.props.firebase.authUser.uid))
 .doc(this.props.firebase.db.collection('users').doc(this.props.authUser.uid))
     .get()
     .then(doc => {
         this.setState({ name: doc.data().name });
       loading: false,
      });  
  }                  

When I try this, I get an error that says:

FirebaseError: Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: a custom DocumentReference object

NEXT ATTEMPT People below seem to find something helpful in the first proposed solution. I haven't been able to find anything useful in it, but reading back through its suggestions, Im struggling to see how the example in the firebase documentation (it does not disclose how to give a :uid value to the .doc() request), which is as follows:

db.collection("cities").doc("SF");

  docRef.get().then(function(doc) {
      if (doc.exists) {
          console.log("Document data:", doc.data());
      } else {
          // doc.data() will be undefined in this case
          console.log("No such document!");
      }

is fundamentally different to my attempt in the componentDidMount function, which is:

this.unsubscribe =
  this.props.firebase.db
    .collection('users')
    // .doc(this.props.firebase.db.collection('users').doc(this.props.firebase.authUser.uid))
    // .doc(this.props.firebase.db.collection('users').uid: this.props.firebase.auth().currentUser.uid  )
    .doc(this.props.authUser.uid)
    .get()
    .then(doc => {
        this.setState({ user.name: doc.data().name });
        // loading: false,
      }else {
        // doc.data() will be undefined in this case
        console.log("Can't find this record");
      }

    );  
  }

Maybe solving that step is a clue that will help to move this toward an outcome. Can anyone find any better firestore documentation to show how to get a user collection record using a logged in user listener uid?

To that end, I can see from the FriendlyEats code lab example, that there is an attempt to give a doc.id to the id search value in the code. I don't know what language this code is written in - but it does look similar to what Im trying to do - I just can't see how to move from that example to something that I know how to work with.

display: function(doc) {
      var data = doc.data();
      data['.id'] = doc.id;
      data['go_to_restaurant'] = function() {
        that.router.navigate('/restaurants/' + doc.id);
      };
Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
Mel
  • 2,481
  • 26
  • 113
  • 273
  • FYI your terminology is not quite right, and making this question difficult to read. There is nothing in Firebase called a "table". In Firebase Auth, there are just users - no "Authentication table". In Firestore, there are collections, and documents within those collections, but no tables. I'm trying to figure out where you're getting stuck and how the code you've shown doesn't work the way you expect, but I'm just not piecing it together. Consider editing the question to be use more standard terminology that you'd find the docs, and be more clear about what isn't working as you expect. – Doug Stevenson Jan 09 '20 at 05:24
  • Fine - happy to substitute tables for collections. The point is still the same. – Mel Jan 09 '20 at 06:33
  • My main point is that I couldn't really get your point, and the terminology didn't help. Could you explain in more detail what doesn't work in the code that you showed? Did something not work as expected? Any errors or debug logs to illustrate? – Doug Stevenson Jan 09 '20 at 06:34
  • Nothing works. I'm expecting to find a way to access the user collection details from the authUser listener. authUser is defined in the context handler and related class method that listens for changes on the method. I can't get past the attributes in the authentication collection - I'm trying to get access to the related user collection in firestore. No logs. Just error messages that say the field is undefined. – Mel Jan 09 '20 at 06:40
  • So, when you run your code, as shown, it just does nothing? Or something that you could log to verify that something or anything happened? – Doug Stevenson Jan 09 '20 at 06:41
  • just the error message - the console logs undefined when I try to find the related user – Mel Jan 09 '20 at 06:42
  • Please edit the question to show the full error log, and help us narrow it down to a line of code. That's essential. – Doug Stevenson Jan 09 '20 at 06:42
  • The line of code is any attempt to find an access point that uses the user collection attributes. All of the attempts I have dreamed up don't work - because they can't be found. – Mel Jan 09 '20 at 06:43
  • In that case, I suggest you start with the documentation for Firestore, as it will walk you through how to get documents and query collections. If you haven't done that yet, nothing will make sense. You should be able to take that and construct a query that works. If not, then we need to see the error or outcome that isn't expected. https://firebase.google.com/docs/firestore/query-data/get-data – Doug Stevenson Jan 09 '20 at 06:46
  • I have been stuck trawling through the documentation on firestore for weeks - trying to solve this problem along with others that are covered by the documentation but for which the solutions shown in the documentation do not work. I have reported these to firestore feedback (the response from firestore is it is beyond the scope of firebase to find solutions that work in react). Hopefully someone sees this message that knows how to get from the authentication date to the user collection data. – Mel Jan 09 '20 at 06:51
  • 1
    I suggest starting with a simple task, get that to work in order to gain some experience, then apply that to more complex problems like the one you have now. There is nothing fundamentally incorrect about the documentation (I know, because I use it all the time, and I help people with the same). In order to get helped on Stack Overflow, you will need to illustrate a specific problem, ideally a MCVE that anyone can use to reproduce the issue. Just saying "I can't get it to work" isn't enough. https://stackoverflow.com/help/minimal-reproducible-example – Doug Stevenson Jan 09 '20 at 06:55

4 Answers4

6

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).

Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
  • So is there something wrong with the way I have defined onAuthUserListener? Then if I try your amendments to that method, what am I supposed to do to get to the user collection from authUser? – Mel Jan 09 '20 at 06:38
  • "So is there something wrong with the way I have defined onAuthUserListener? " -> not from what I can see. "what am I supposed to do to get to the user collection from authUser?" -> If I understand correctly you want to get ONE document, not the collection. The code in the answer should work. – Renaud Tarnec Jan 09 '20 at 06:43
  • I want to get authUser - is your code an improvement on my attempt? I can't find a way that works to get the authUser to give me access to the user collection that has the same uid. I'm trying to understand your code suggestion - for how that improves on mine as a first step. Please can you identify which bit of it is the improvement/correction? Thank you – Mel Jan 09 '20 at 06:45
  • What happens if you do `this.auth.onAuthStateChanged(authUser => { if (authUser) {console.log(authUser.uid) })`? – Renaud Tarnec Jan 09 '20 at 06:50
  • I can output all the authUser properties (the authentication collection data). I cannot get to the user data from the related document in the user collection that has the uid as the id – Mel Jan 09 '20 at 06:53
  • Can you confirm that the `console.log()` above prints the user's uid? – Renaud Tarnec Jan 09 '20 at 06:56
  • confirmed - it does – Mel Jan 09 '20 at 06:58
  • There was a typo in my code: please change from `db.collection(...)` to `this.db.collection()`in the code of the answer. – Renaud Tarnec Jan 09 '20 at 06:59
  • So are you suggesting I use another listener inside the Test function - or is there something wrong with the listener I already wrote? – Mel Jan 09 '20 at 07:06
  • And then - when the listener issues is resolved, how do I return the value of the user attributes (from the user collection - ie user.name)? – Mel Jan 09 '20 at 07:06
  • The modifications I proposed are to be done in your existing code (existing listener). I am not really verse in React so I cannot tell you if your `onAuthUserListener = (next, fallback) => ()` is correct but if you get the correct `uid` when you do `this.auth.onAuthStateChanged(authUser => { if (authUser) {console.log(authUser.uid) })`, replacing `console.log(authUser.uid)` with the code of my answer should work. I'll adapt the answer to reflect that the changes are to be implemented in your React listener. – Renaud Tarnec Jan 09 '20 at 07:10
  • "how do I return the value of the user attributes (from the user collection - ie user.name)" -> it will become clear when you get the result of `console.log(userData);`. As explained in the [doc](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentSnapshot#data) the `data()` methods "retrieves all fields in the document as an Object". So you just need to do `userData.name`. It worths reading the doc in detail, as all of this is explained there. – Renaud Tarnec Jan 09 '20 at 07:15
  • I have read the docs - in detail - several times. The reason I'm asking in this forum is that the docs don't work. This has been consistent experience over the last month. The Firebase support team at google disclaim responsibility for firebase working with other tools - so there must be something about react that is incompatible with firebase. – Mel Jan 09 '20 at 11:22
  • Thanks anyway for trying to help. – Mel Jan 09 '20 at 11:23
  • @Mel Have you tried the proposed solution? Does it work?? – Renaud Tarnec Jan 09 '20 at 15:26
  • No - it does not work. The existing listener I already have gives me the ability to log the values in the Authentication collection for the uid. I'm not improving on that with the alternate way of structuring the listener that you have suggested and I'm also not getting access to the attributes in the user collection. Thanks anyway for the suggestions and the effort to help. – Mel Jan 09 '20 at 21:01
  • This didn't work for me - but some people have voted on this solution and seem to have found it helpful - so Im giving you the points. Thank you just the same for trying to help. – Mel Jan 18 '20 at 08:45
1

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();
  }
}
samthecodingman
  • 23,122
  • 4
  • 30
  • 54
  • Thank you! I'll try this tonight. I'm excited to learn about this either way. I'll come back shortly with feedback. – Mel Jan 15 '20 at 06:47
  • Hi Sam - thank you for offering this suggestion. I took some time to read through the documentation you linked and I've had a few goes at this. While I'm grateful for the help - this didn't solve my problem. When I try to access the user collection attributes, I am still getting an error that says: TypeError: Cannot read property 'user' of undefined – Mel Jan 15 '20 at 22:29
  • @Mel When you ran the original code, were you notified of the TypeError? This is the first time you have mentioned it. Which means this code did it's job of throwing the error outside of the Promise's scope. Can you provide the output of `console.log(snapshot.data())`? – samthecodingman Jan 15 '20 at 22:51
  • I tried this - the error message says: TypeError: snapshot.data is not a function – Mel Jan 15 '20 at 23:58
  • I'll keep trying to move it around - maybe I'm not trying to log this in a good spot – Mel Jan 15 '20 at 23:58
  • I added the output of the authUser log - which is different to what it was before I made these changes. I can't find a way to log snapshot.data() yet - but I'll keep trying to do that. – Mel Jan 16 '20 at 00:53
  • @Mel You should be able to log it just below `const dbUser = snapshot.data();` in your code. Alternatively try `console.log(dbUser)`. What is `app`? Is it `import * as app from 'firebase/app'` or something else? – samthecodingman Jan 16 '20 at 07:59
  • You can also message me on the [Firebase Slack](https://firebase.community) which will allow us to work through it step by step and message screenshots. – samthecodingman Jan 16 '20 at 08:01
  • import app from 'firebase/app'; class Firebase { constructor() { app.initializeApp(config).firestore(); – Mel Jan 16 '20 at 22:10
  • I don't know why I can't get the console to log the output. This is a change that I think coincides with the changes to the auth listener. Previously, I was at least able to log the authUser.email - now I can't get that output, but I can get console.log(authUser). I'll keep trying to figure this step out and try to solve for why I can't log dbUser – Mel Jan 16 '20 at 22:11
  • I can do console.log(user) - but it returns undefined. – Mel Jan 16 '20 at 22:18
  • I know I am logged in because the navigation bar has alternative menus for logged in and logged out - and it is rendering logged in nabber - from the AuthContext.Consumer – Mel Jan 16 '20 at 22:19
  • I'm starting to think your code is crashing because of `React.createContext(null)`, where the initial value is `null` (and your consumer doesn't null-check) causing it to crash before your `onAuthStateChanged` event handler even gets the chance to run. – samthecodingman Jan 17 '20 at 14:11
  • but it works just fine in all of the other use cases – Mel Jan 17 '20 at 16:28
  • If that were the problem - I would have thought I might not be able to log the authUser in the code. I can - I just can't use its attributes and I definitely can't use them the way its shown in the Firebase documentation in any of the rest of my code. – Mel Jan 17 '20 at 16:34
  • We didn't get a solution, but I'm grateful for the help in trying to figure this out. Thank you – Mel Jan 18 '20 at 08:46
0

In your AuthContext.Provider implementation, you access the SDK's onAuthStateChanged listener directly:

componentDidMount() {
  this.listener = this.props.firebase.auth.onAuthStateChanged(
    authUser => {
      authUser
        ? this.setState({ authUser })
        : this.setState({ authUser: null });
    }
  );
}

This should be changed to use the onAuthUserListener from your helper class:

componentDidMount() {
  this.listener = this.props.firebase.onAuthUserListener(
    /* next()     */ (authUserWithData) => this.setState({authUser: authUserWithData}),
    /* fallback() */ () => this.setState({authUser: null})
  );
}

Regarding log messages filled with lots of random properties, this is because the firebase.User object has both a public API and an implementation with a number of private properties and methods that are minified when compiled. Because these minified properties and methods are not explicitly marked as not enumerable, they are included in any log output. If you instead wanted to only log the parts that are actually useful you can destructure and restructure the object using:

// Extracts public properties of firebase.User objects
// see https://firebase.google.com/docs/reference/js/firebase.User#properties
function extractPublicProps(user) {
  let {displayName, email, emailVerified, isAnonymous, metadata, phoneNumber, photoURL, providerData, providerId, refreshToken, tenantId, uid} = user;
  return {displayName, email, emailVerified, isAnonymous, metadata, phoneNumber, photoURL, providerData, providerId, refreshToken, tenantId, uid}
}

function extractUsefulProps(user) {
  let {displayName, email, emailVerified, isAnonymous, phoneNumber, photoURL, uid} = user;
  return {displayName, email, emailVerified, isAnonymous, phoneNumber, photoURL, uid}
}

let authUser = firebase.auth().currentUser;
console.log(authUser);
console.log(extractPublicProps(authUser));
console.log(extractUsefulProps(authUser));
samthecodingman
  • 23,122
  • 4
  • 30
  • 54
  • Thanks @samthecodingman for continuing to try to help me. I had a look at this. My objective is to read the uid of the authUser so that I can use it to get attributes of the related user collection for that user (name in the user collection has more than displayName in the firebase auth collection - so I'm not trying to read properties of the Authentication table. – Mel Jan 25 '20 at 02:09
  • The suggested change to the listener componentDidMount function did not throw an error - but it did not work. When it try to log the value of authUser in the dashboard component using this listener - I get an error saying authUser is not defined. I don't get this error when I use the componentDidMount for the AuthContext.Provider in the way I had it defined. Thanks for the information about the random properties in the log messages. – Mel Jan 25 '20 at 02:09
  • @Mel Can you confirm that the last line of your `Dashboard` file is `export default withAuthentication(Dashboard);` (and not `withFirebase`) – samthecodingman Jan 25 '20 at 07:40
  • Confirmed. withFirebase is incorporated in withAuthentication - so it gets picked up through that HOC. – Mel Jan 25 '20 at 07:43
  • @Mel Can you check the messages I sent you on Slack – samthecodingman Jan 25 '20 at 07:50
  • Although the answer still escapes me, i'm grateful for your efforts to try to help me figure this out. – Mel Jan 25 '20 at 20:09
0

If anyone else is similarly stuck, I found the solution here: Firebase & React: CollectionReference.doc() argument type

It doesn't work on page refresh (still throws an error that the uid is null) but react hooks to useEffect should replace the componentDidMount function with a combination of Mount and Update. Im trying that next.

Mel
  • 2,481
  • 26
  • 113
  • 273