Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase listener with React Hooks

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'; import { FirebaseContext } from '../Firebase/ContextHookProvider'; import useFirebaseAuthentication from '../Firebase/ContextHook';   const Dashboard2 = (props) => {     const firebase = useContext(FirebaseContext);     const authUser =  useFirebaseAuthentication(firebase);      return (         <div>authUser.email</div>     )  }   export default Dashboard2; 

And I try to use it as a route with no components or auth wrapper:

<Route path={ROUTES.DASHBOARD2} component={Dashboard2} /> 

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

Attempted import error: 'FirebaseContext' is not exported from '../Firebase/ContextHookProvider'.

That error message makes sense, because ContextHookProvider does not export FirebaseContext - it exports FirebaseProvider - but if I don't try to import this in Dashboard2 - then I can't access it in the function that tries to use it.

One side effect of this attempt is that my sign up method no longer works. It now generates an error message that says:

TypeError: Cannot read property 'doCreateUserWithEmailAndPassword' of null

I'll solve this problem later- but there must be a way to figure out how to use react with firebase that does not involve months of this loop through millions of avenues that don't work to get a basic auth setup. Is there a starter kit for firebase (firestore) that works with react hooks?

Next attempt I tried to follow the approach in this udemy course- but it only works to generate a form input - there isn't a listener to put around the routes to adjust with the authenticated user.

I tried to follow the approach in this youtube tutorial - which has this repo to work from. It shows how to use hooks, but not how to use context.

NEXT ATTEMPT I found this repo that seems to have a well thought out approach to using hooks with firestore. However, I can't make sense of the code.

I cloned this - and tried to add all the public files and then when I run it - I can't actually get the code to operate. I'm not sure what's missing from the instructions for how to get this to run in order to see if there are lessons in the code that can help solve this problem.

NEXT ATTEMPT

I bought the divjoy template, which is advertised as being setup for firebase (it isn't setup for firestore in case anyone else is considering this as an option).

That template proposes an auth wrapper that initialises the config of the app - but just for the auth methods - so it needs to be restructured to allow another context provider for firestore. When you muddle through that process and use the process shown in this post, what's left is an error in the following callback:

useEffect(() => {     const unsubscribe = firebase.auth().onAuthStateChanged(user => {       if (user) {         setUser(user);       } else {         setUser(false);       }     }); 

It doesn't know what firebase is. That's because it's defined in the firebase context provider which is imported and defined (in the useProvideAuth() function) as:

  const firebase = useContext(FirebaseContext) 

Without chances to the callback, the error says:

React Hook useEffect has a missing dependency: 'firebase'. Either include it or remove the dependency array

Or, if I try and add that const to the callback, I get an error that says:

React Hook "useContext" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

NEXT ATTEMPT

I have reduced my firebase config file down to just config variables (I will write helpers in the context providers for each context I want to use).

import firebase from 'firebase/app'; import 'firebase/auth'; import 'firebase/firestore';  const devConfig = {     apiKey: process.env.REACT_APP_DEV_API_KEY,     authDomain: process.env.REACT_APP_DEV_AUTH_DOMAIN,     databaseURL: process.env.REACT_APP_DEV_DATABASE_URL,     projectId: process.env.REACT_APP_DEV_PROJECT_ID,     storageBucket: process.env.REACT_APP_DEV_STORAGE_BUCKET,     messagingSenderId: process.env.REACT_APP_DEV_MESSAGING_SENDER_ID,     appId: process.env.REACT_APP_DEV_APP_ID    };     const prodConfig = {     apiKey: process.env.REACT_APP_PROD_API_KEY,     authDomain: process.env.REACT_APP_PROD_AUTH_DOMAIN,     databaseURL: process.env.REACT_APP_PROD_DATABASE_URL,     projectId: process.env.REACT_APP_PROD_PROJECT_ID,     storageBucket: process.env.REACT_APP_PROD_STORAGE_BUCKET,     messagingSenderId:  process.env.REACT_APP_PROD_MESSAGING_SENDER_ID,     appId: process.env.REACT_APP_PROD_APP_ID   };    const config =     process.env.NODE_ENV === 'production' ? prodConfig : devConfig;   class Firebase {   constructor() {     firebase.initializeApp(config);     this.firebase = firebase;     this.firestore = firebase.firestore();     this.auth = firebase.auth();   } };  export default Firebase;   

I then have an auth context provider as follows:

import React, { useState, useEffect, useContext, createContext } from "react"; import Firebase from "../firebase";  const authContext = createContext();  // Provider component that wraps app and makes auth object ... // ... available to any child component that calls useAuth(). export function ProvideAuth({ children }) {   const auth = useProvideAuth();    return <authContext.Provider value={auth}>{children}</authContext.Provider>; }  // Hook for child components to get the auth object ... // ... and update when it changes. export const useAuth = () => {    return useContext(authContext); };  // Provider hook that creates auth object and handles state function useProvideAuth() {   const [user, setUser] = useState(null);     const signup = (email, password) => {     return Firebase       .auth()       .createUserWithEmailAndPassword(email, password)       .then(response => {         setUser(response.user);         return response.user;       });   };    const signin = (email, password) => {     return Firebase       .auth()       .signInWithEmailAndPassword(email, password)       .then(response => {         setUser(response.user);         return response.user;       });   };      const signout = () => {     return Firebase       .auth()       .signOut()       .then(() => {         setUser(false);       });   };    const sendPasswordResetEmail = email => {     return Firebase       .auth()       .sendPasswordResetEmail(email)       .then(() => {         return true;       });   };    const confirmPasswordReset = (password, code) => {     // Get code from query string object     const resetCode = code || getFromQueryString("oobCode");      return Firebase       .auth()       .confirmPasswordReset(resetCode, password)       .then(() => {         return true;       });   };    // Subscribe to user on mount   useEffect(() => {      const unsubscribe = firebase.auth().onAuthStateChanged(user => {       if (user) {         setUser(user);       } else {         setUser(false);       }     });      // Subscription unsubscribe function     return () => unsubscribe();   }, []);    return {     user,     signup,     signin,     signout,     sendPasswordResetEmail,     confirmPasswordReset   }; }  const getFromQueryString = key => {   return queryString.parse(window.location.search)[key]; }; 

I also made a firebase context provider as follows:

import React, { createContext } from 'react'; import Firebase from "../../firebase";  const FirebaseContext = createContext(null) export { FirebaseContext }   export default ({ children }) => {      return (       <FirebaseContext.Provider value={ Firebase }>         { children }       </FirebaseContext.Provider>     )   } 

Then, in index.js I wrap the app in the firebase provider

ReactDom.render(     <FirebaseProvider>         <App />     </FirebaseProvider>,  document.getElementById("root"));  serviceWorker.unregister(); 

and in my routes list, I have wrapped the relevant routes in the auth provider:

import React from "react"; import IndexPage from "./index"; import { Switch, Route, Router } from "./../util/router.js";  import { ProvideAuth } from "./../util/auth.js";  function App(props) {   return (     <ProvideAuth>       <Router>         <Switch>           <Route exact path="/" component={IndexPage} />            <Route             component={({ location }) => {               return (                 <div                   style={{                     padding: "50px",                     width: "100%",                     textAlign: "center"                   }}                 >                   The page <code>{location.pathname}</code> could not be found.                 </div>               );             }}           />         </Switch>       </Router>     </ProvideAuth>   ); }  export default App; 

On this particular attempt, I'm back to the problem flagged earlier with this error:

TypeError: _firebase__WEBPACK_IMPORTED_MODULE_2__.default.auth is not a function

It points to this line of the auth provider as creating the problem:

useEffect(() => {      const unsubscribe = firebase.auth().onAuthStateChanged(user => {       if (user) {         setUser(user);       } else {         setUser(false);       }     }); 

I have tried using capitalised F in Firebase and it generates the same error.

When I try Tristan's advice, I remove all of those things and try and define my unsubscribe method as an unlisten method (I don't know why he isn't using the firebase language - but if his approach worked, I'd try harder to figure out why). When I try to use his solution, the error message says:

TypeError: _util_contexts_Firebase__WEBPACK_IMPORTED_MODULE_8___default(...) is not a function

The answer to this post suggests removing () from after auth. When I try that, I get an error that says:

TypeError: Cannot read property 'onAuthStateChanged' of undefined

However this post suggest a problem with the way firebase is imported in the auth file.

I have it imported as: import Firebase from "../firebase";

Firebase is the name of the class.

The videos Tristan recommended are helpful background, but I'm currently on episode 9 and still not found the part that is supposed to help solve this problem. Does anyone know where to find that?

NEXT ATTEMPT Next - and trying to solve the context problem only - I have imported both createContext and useContext and tried to use them as shown in this documentation.

I can't get passed an error that says:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: ...

I have been through the suggestions in this link to try and solve this problem and cannot figure it out. I don't have any of the problems shown in this trouble shooting guide.

Currently - the context statement looks as follows:

import React, {  useContext } from 'react'; import Firebase from "../../firebase";     export const FirebaseContext = React.createContext();    export const useFirebase = useContext(FirebaseContext);    export const FirebaseProvider = props => (     <FirebaseContext.Provider value={new Firebase()}>       {props.children}     </FirebaseContext.Provider>   );   

I spent time using this udemy course to try and figure out the context and hooks element to this problem - after watching it - the only aspect to the solution proposed by Tristan below is that the createContext method isn't called correctly in his post. it needs to be "React.createContext" but it still doesn't get anywhere close to solving the problem.

I'm still stuck.

Can anyone see what's gone awry here?

like image 370
Mel Avatar asked Jan 30 '20 01:01

Mel


People also ask

Can we connect Firebase with React?

To integrate Firebase into our React app, we need to first get the web configuration object and then use it to initialize Firebase in our react app. Copy the config to the clipboard; we'll need it later on to initialize Firebase. Then, click Continue to console to complete the process.

What are React Firebase hooks?

React Firebase Hooks provides convenience listeners for Collections and Documents stored with Cloud Firestore. The hooks wrap around the firestore.

Can we use React Firebase hooks in react native?

Is react-native-firebase-hooks safe to use? The npm package react-native-firebase-hooks was scanned for known vulnerabilities and missing license, and no issues were found. Thus the package was deemed as safe to use.


2 Answers

EDIT (March 3rd, 2020):

Let's start from scratch.

  1. I've created a new project:

    yarn create react-app firebase-hook-issue

  2. I've deleted all 3 App* files created by default, removed import from index.js and also removed service worker to have a clean index.js like that:

import React from 'react'; import ReactDOM from 'react-dom'; import './index.css';  const App = () => {     return (         <div>             Hello Firebase!                     </div>     ); };  ReactDOM.render(<App />, document.getElementById('root')); 
  1. I've started the app to see that Hello Firebase! is printed out.
  2. I've added firebase module
yarn add firebase 
  1. I've run firebase init to setup firebase for that project. I've picked one of my empty firebase projects and I have selected Database and Firestore, which end up in creating next files:
.firebaserc database.rules.json firebase.json firestore.indexes.json firestore.rules 
  1. I've added imports for firebase libs and also created a Firebase class and FirebaseContext. At last I've wrapped the App in FirebaseContext.Provider component and set its value to a new Firebase() instance. This was we gonna have only one instance of Firebase app instantiated as we should because it must be a singleton:
import React from "react"; import ReactDOM from "react-dom"; import "./index.css";  import app from "firebase/app"; import "firebase/database"; import "firebase/auth"; import "firebase/firestore";  class Firebase {     constructor() {         app.initializeApp(firebaseConfig);          this.realtimedb = app.database();         this.firestore = app.firestore();     } }  const FirebaseContext = React.createContext(null);  const firebaseConfig = {     apiKey: "",     authDomain: "",     databaseURL: "",     projectId: "",     storageBucket: "",     messagingSenderId: "",     appId: "", };  const App = () => {     return <div>Hello Firebase!</div>; };  ReactDOM.render(     <FirebaseContext.Provider value={new Firebase()}>         <App />     </FirebaseContext.Provider>     , document.getElementById("root")); 
  1. Let's verify that we can read anything from Firestore. To verify just the reading first, I went to my project in Firebase Console, open my Cloud Firestore database and added a new collection called counters with a document simple containing one field called value of type number and value 0. enter image description here enter image description here

  2. Then I've updated App class to use FirebaseContext we have created, made useState hook for our simple counter hook and used useEffect hook to read the value from the firestore:

import React from "react"; import ReactDOM from "react-dom"; import "./index.css";  import app from "firebase/app"; import "firebase/database"; import "firebase/auth"; import "firebase/firestore";  const firebaseConfig = {     apiKey: "",     authDomain: "",     databaseURL: "",     projectId: "",     storageBucket: "",     messagingSenderId: "",     appId: "", };  class Firebase {     constructor() {         app.initializeApp(firebaseConfig);          this.realtimedb = app.database();         this.firestore = app.firestore();     } }  const FirebaseContext = React.createContext(null);  const App = () => {     const firebase = React.useContext(FirebaseContext);     const [counter, setCounter] = React.useState(-1);      React.useEffect(() => {         firebase.firestore.collection("counters").doc("simple").get().then(doc => {             if(doc.exists) {                 const data = doc.data();                 setCounter(data.value);             } else {                 console.log("No such document");             }         }).catch(e => console.error(e));     }, []);      return <div>Current counter value: {counter}</div>; };  ReactDOM.render(     <FirebaseContext.Provider value={new Firebase()}>         <App />     </FirebaseContext.Provider>     , document.getElementById("root"));  

Note: To keep the answer as short as possible I've made sure you don't need to be authenticated with firebase, by setting access to the firestore to be in test mode (firestore.rules file):

rules_version = '2'; service cloud.firestore {   match /databases/{database}/documents {      // This rule allows anyone on the internet to view, edit, and delete     // all data in your Firestore database. It is useful for getting     // started, but it is configured to expire after 30 days because it     // leaves your app open to attackers. At that time, all client     // requests to your Firestore database will be denied.     //     // Make sure to write security rules for your app before that time, or else     // your app will lose access to your Firestore database     match /{document=**} {       allow read, write: if request.time < timestamp.date(2020, 4, 8);     }   } } 

My previous answer: You are more than welcome to take a look at my react-firebase-auth-skeleton:

https://github.com/PompolutZ/react-firebase-auth-skeleton

It mainly follows the article:

https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial

But somewhat rewritten to use hooks. I have used it in at least two of my projects.

Typical usage from my current pet project:

import React, { useState, useEffect, useContext } from "react"; import ButtonBase from "@material-ui/core/ButtonBase"; import Typography from "@material-ui/core/Typography"; import DeleteIcon from "@material-ui/icons/Delete"; import { FirebaseContext } from "../../../firebase"; import { useAuthUser } from "../../../components/Session"; import { makeStyles } from "@material-ui/core/styles";  const useStyles = makeStyles(theme => ({     root: {         flexGrow: 1,         position: "relative",         "&::-webkit-scrollbar-thumb": {             width: "10px",             height: "10px",         },     },      itemsContainer: {         position: "absolute",         top: 0,         left: 0,         right: 0,         bottom: 0,         display: "flex",         alignItems: "center",         overflow: "auto",     }, }));  export default function LethalHexesPile({     roomId,     tokens,     onSelectedTokenChange, }) {     const classes = useStyles();     const myself = useAuthUser();     const firebase = useContext(FirebaseContext);     const pointyTokenBaseWidth = 95;     const [selectedToken, setSelectedToken] = useState(null);      const handleTokenClick = token => () => {         setSelectedToken(token);         onSelectedTokenChange(token);     };      useEffect(() => {         console.log("LethalHexesPile.OnUpdated", selectedToken);     }, [selectedToken]);      const handleRemoveFromBoard = token => e => {         console.log("Request remove token", token);         e.preventDefault();         firebase.updateBoardProperty(roomId, `board.tokens.${token.id}`, {             ...token,             isOnBoard: false,             left: 0,             top: 0,             onBoard: { x: -1, y: -1 },         });         firebase.addGenericMessage2(roomId, {             author: "Katophrane",             type: "INFO",             subtype: "PLACEMENT",             value: `${myself.username} removed lethal hex token from the board.`,         });     };      return (         <div className={classes.root}>             <div className={classes.itemsContainer}>                 {tokens.map(token => (                     <div                         key={token.id}                         style={{                             marginRight: "1rem",                             paddingTop: "1rem",                             paddingLeft: "1rem",                             filter:                             selectedToken &&                             selectedToken.id === token.id                                 ? "drop-shadow(0 0 10px magenta)"                                 : "",                             transition: "all .175s ease-out",                         }}                         onClick={handleTokenClick(token)}                     >                         <div                             style={{                                 width: pointyTokenBaseWidth * 0.7,                                 position: "relative",                             }}                         >                             <img                                 src={`/assets/tokens/lethal.png`}                                 style={{ width: "100%" }}                             />                             {selectedToken && selectedToken.id === token.id && (                                 <ButtonBase                                     style={{                                         position: "absolute",                                         bottom: "0%",                                         right: "0%",                                         backgroundColor: "red",                                         color: "white",                                         width: "2rem",                                         height: "2rem",                                         borderRadius: "1.5rem",                                         boxSizing: "border-box",                                         border: "2px solid white",                                     }}                                     onClick={handleRemoveFromBoard(token)}                                 >                                     <DeleteIcon                                         style={{                                             width: "1rem",                                             height: "1rem",                                         }}                                     />                                 </ButtonBase>                             )}                         </div>                         <Typography>{`${token.id}`}</Typography>                     </div>                 ))}             </div>         </div>     ); } 

Two most important parts here are: - useAuthUser() hook which provides current authenticated user. - FirebaseContext which I use via useContext hook.

const firebase = useContext(FirebaseContext); 

When you have context to firebase, its up to you to implement firebase object to your liking. Sometimes I do write some helpful functions, sometimes its easier to just setup listeners right in the useEffect hook I create for my current component.

One of the best parts of that article was creation of withAuthorization HOC, which allows you to specify the prerequisites for accessing the page either in component itself:

const condition = authUser => authUser && !!authUser.roles[ROLES.ADMIN]; export default withAuthorization(condition)(AdminPage); 

Or maybe even settings those conditions right in your router implementation.

Hope that looking at the repo and article will give you some extra good thoughts to enhance other brilliant answers to your question.

like image 34
fxdxpz Avatar answered Sep 19 '22 07:09

fxdxpz


Major Edit: Took some time to look into this a bit more this is what I have come up with is a cleaner solution, someone might disagree with me about this being a good way to approach this.

UseFirebase Auth Hook

import { useEffect, useState, useCallback } from 'react'; import firebase from 'firebase/app'; import 'firebase/auth';  const firebaseConfig = {   apiKey: "xxxxxxxxxxxxxx",   authDomain: "xxxx.firebaseapp.com",   databaseURL: "https://xxxx.firebaseio.com",   projectId: "xxxx",   storageBucket: "xxxx.appspot.com",   messagingSenderId: "xxxxxxxx",   appId: "1:xxxxxxxxxx:web:xxxxxxxxx" };  firebase.initializeApp(firebaseConfig)  const useFirebase = () => {   const [authUser, setAuthUser] = useState(firebase.auth().currentUser);    useEffect(() => {     const unsubscribe = firebase.auth()       .onAuthStateChanged((user) => setAuthUser(user))     return () => {       unsubscribe()     };   }, []);    const login = useCallback((email, password) => firebase.auth()     .signInWithEmailAndPassword(email, password), []);    const logout = useCallback(() => firebase.auth().signOut(), [])    return { login, authUser, logout } }  export { useFirebase } 

If authUser is null then not authenticated, if user has a value, then authenticated.

firebaseConfig can be found on the firebase Console => Project Settings => Apps => Config Radio Button

useEffect(() => {   const unsubscribe = firebase.auth()     .onAuthStateChanged(setAuthUser)    return () => {     unsubscribe()   }; }, []); 

This useEffect hook is the core to tracking the authChanges of a user. We add a listener to the onAuthStateChanged event of firebase.auth() that updates the value of authUser. This method returns a callback for unsubscribing this listener which we can use to clean up the listener when the useFirebase hook is refreshed.

This is the only hook we need for firebase authentication (other hooks can be made for firestore etc.

const App = () => {   const { login, authUser, logout } = useFirebase();    if (authUser) {     return <div>       <label>User is Authenticated</label>       <button onClick={logout}>Logout</button>     </div>   }    const handleLogin = () => {     login("[email protected]", "password0");   }    return <div>     <label>User is not Authenticated</label>     <button onClick={handleLogin}>Log In</button>   </div> } 

This is a basic implementation of the App component of a create-react-app

useFirestore Database Hook

const useFirestore = () => {   const getDocument = (documentPath, onUpdate) => {     firebase.firestore()       .doc(documentPath)       .onSnapshot(onUpdate);   }    const saveDocument = (documentPath, document) => {     firebase.firestore()       .doc(documentPath)       .set(document);   }    const getCollection = (collectionPath, onUpdate) => {     firebase.firestore()       .collection(collectionPath)       .onSnapshot(onUpdate);   }    const saveCollection = (collectionPath, collection) => {     firebase.firestore()       .collection(collectionPath)       .set(collection)   }    return { getDocument, saveDocument, getCollection, saveCollection } } 

This can be implemented in your component like so:

const firestore = useFirestore(); const [document, setDocument] = useState();  const handleGet = () => {   firestore.getDocument(     "Test/ItsWFgksrBvsDbx1ttTf",      (result) => setDocument(result.data())   ); }  const handleSave = () => {   firestore.saveDocument(     "Test/ItsWFgksrBvsDbx1ttTf",      { ...document, newField: "Hi there" }   ); 

}

This then removes the need for the React useContext as we get updates directly from firebase itself.

Notice a couple of things:

  1. Saving an unchanged document does not trigger a new snapshot so "oversaving" doesn't cause rerenders
  2. On calling getDocument the callback onUpdate is called straight away with an initial "snapshot" so you don't need extra code for getting the initial state of the document.

Edit has removed a large chunk of the old answer

like image 131
Tristan Trainer Avatar answered Sep 20 '22 07:09

Tristan Trainer