I am trying to figure out how to use a Firebase listener so that cloud firestore data is refreshed with react hooks updates.
Initially, I made this using a class component with a componentDidMount function to get the firestore data.
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,
});
}
That breaks when the page updates, so I am trying to figure out how to move the listener to react hooks.
I have installed the react-firebase-hooks tool - although I can't figure out how to read the instructions to be able to get it to work.
I have a function component as follows:
import React, { useState, useEffect } from 'react';
import { useDocument } from 'react-firebase-hooks/firestore';
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, withAuthentication } from '../Session/Index';
function Dashboard2(authUser) {
const FirestoreDocument = () => {
const [value, loading, error] = useDocument(
Firebase.db.doc(authUser.uid),
//firebase.db.doc(authUser.uid),
//firebase.firestore.doc(authUser.uid),
{
snapshotListenOptions: { includeMetadataChanges: true },
}
);
return (
<div>
<p>
{error && <strong>Error: {JSON.stringify(error)}</strong>}
{loading && <span>Document: Loading...</span>}
{value && <span>Document: {JSON.stringify(value.data())}</span>}
</p>
</div>
);
}
}
export default withAuthentication(Dashboard2);
This component is wrapped in an authUser wrapper at the route level as follows:
<Route path={ROUTES.DASHBOARD2} render={props => (
<AuthUserContext.Consumer>
{ authUser => (
<Dashboard2 authUser={authUser} {...props} />
)}
</AuthUserContext.Consumer>
)} />
I have a firebase.js file, which plugs into firestore 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();
}
It also defines a listener to know when the authUser changes:
onAuthUserListener(next, fallback) {
// onUserDataListener(next, fallback) {
return this.auth.onAuthStateChanged(authUser => {
if (authUser) {
this.user(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('An error occured -> ', err.code ? err.code + ': ' + err.message : (err.message || err));
setTimeout(fallback, 0); // escapes this Promise's error handler
});
};
if (!authUser) {
// user not logged in, call fallback handler
fallback();
return;
}
});
};
Then, in my firebase context setup I have:
import FirebaseContext, { withFirebase } from './Context';
import Firebase from '../../firebase';
export default Firebase;
export { FirebaseContext, withFirebase };
The context is setup in a withFirebase wrapper as follows:
import React from 'react';
const FirebaseContext = React.createContext(null);
export const withFirebase = Component => props => (
<FirebaseContext.Consumer>
{firebase => <Component {...props} firebase={firebase} />}
</FirebaseContext.Consumer>
);
export default FirebaseContext;
Then, in my withAuthentication HOC, I have a context provider as:
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;
Currently - when I try this, I get an error in the Dashboard2 component that says:
Firebase' is not defined
I tried lowercase firebase and get the same error.
I also tried firebase.firestore and Firebase.firestore. I get the same error.
I'm wondering if I can't use my HOC with a function component?
I have seen this demo app and this blog post.
Following the advice in the blog, I made a new firebase/contextReader.jsx with:
import React, { useEffect, useContext } from 'react';
import Firebase from '../../firebase';
export const userContext = React.createContext({
user: null,
})
export const useSession = () => {
const { user } = useContext(userContext)
return user
}
export const useAuth = () => {
const [state, setState] = React.useState(() =>
{ const user = firebase.auth().currentUser
return { initializing: !user, user, }
}
);
function onChange(user) {
setState({ initializing: false, user })
}
React.useEffect(() => {
// listen for auth state changes
const unsubscribe = firebase.auth().onAuthStateChanged(onChange)
// unsubscribe to the listener when unmounting
return () => unsubscribe()
}, [])
return state
}
Then I try to wrap my App.jsx in that reader with:
function App() {
const { initializing, user } = useAuth()
if (initializing) {
return <div>Loading</div>
}
// )
// }
// const App = () => (
return (
<userContext.Provider value={{ user }}>
<Router>
<Navigation />
<Route path={ROUTES.LANDING} exact component={StandardLanding} />
When I try this, I get an error that says:
TypeError: _firebase__WEBPACK_IMPORTED_MODULE_2__.default.auth is not
a function
I have seen this post dealing with that error and have tried uninstalling and reinstalling yarn. It makes no difference.
When I look at the demo app, it suggests that context should be created using an 'interface' method. I can't see where this is coming from - I can't find a reference to explain it in the documentation.
I can't make sense of the instructions other than to try what I have done to plug this in.
I have seen this post which attempts to listen to firestore without using react-firebase-hooks. The answers point back to trying to figure out how to use this tool.
I have read this excellent explanation which goes into how to move away from HOCs to hooks. I'm stuck with how to integrate the firebase listener.
I have seen this post which provides a helpful example for how to think about doing this. Not sure if I should be trying to do this in the authListener componentDidMount - or in the Dashboard component that is trying to use it.
NEXT ATTEMPT
I found this post, which is trying to solve the same problem.
When I try to implement the solution offered by Shubham Khatri, I set up the firebase config as follows:
A context provider with:
import React, {useContext} from 'react';
import Firebase from '../../firebase';
const FirebaseContext = React.createContext();
export const FirebaseProvider = (props) => (
<FirebaseContext.Provider value={new Firebase()}>
{props.children}
</FirebaseContext.Provider>
);
The context hook then has:
import React, { useEffect, useContext, useState } from 'react';
const useFirebaseAuthentication = (firebase) => {
const [authUser, setAuthUser] = useState(null);
useEffect(() =>{
const unlisten =
firebase.auth.onAuthStateChanged(
authUser => {
authUser
? setAuthUser(authUser)
: setAuthUser(null);
},
);
return () => {
unlisten();
}
});
return authUser
}
export default useFirebaseAuthentication;
Then in the index.js I wrap the App in the provider as:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App/Index';
import {FirebaseProvider} from './components/Firebase/ContextHookProvider';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<FirebaseProvider>
<App />
</FirebaseProvider>,
document.getElementById('root')
);
serviceWorker.unregister();
Then, when I try to use the listener in the component I have:
import React, {useContext} from 'react';
impo