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:
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);
};
If the user login with a custom "email/password" you don't know anything else about that user (apart from the unique user id). If a user login with Facebook, or with Google sign in, you can get other information like the profile picture url. It is explained here: firebase.google.com/docs/auth/android/… .
User information is not stored in the Firestore database, they are associated with the Firebase Authentication which you set up for the log in. To retrieve the related user information, you need to use the related FirebaseAuth APIs. Use this to retrieve the current log in user: FirebaseUser user = FirebaseAuth.
Within Cloud Firestore there is no type that is a "reference to a Firebase Authentication user". But if you store user-specific documents in Firestore, you can use its Document Reference type to reference those user-specific documents. This user is first on the weekly Google Cloud leaderboard.
After authentication, create a child with the UID given by Firebase, and set its value to your user class: //get firebase user FirebaseUser user = FirebaseAuth. getInstance(). getCurrentUser(); //get reference DatabaseReference ref = FirebaseDatabase.
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
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();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With