36

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?

Mel
  • 2,481
  • 26
  • 113
  • 273
  • It's undefined because you are not importing it. – Josh Pittman Jan 30 '20 at 04:36
  • 3
    do you just need to add `export` to `export const FirebaseContext`? – Federkun Feb 02 '20 at 00:15
  • Hi Mel apologise for the slow reply, I've had a crazy two weeks in work so looking at a computer in the evening was out of the question! I have updated my answer for you to provide a clean and very simple solution that you can check. – Tristan Trainer Feb 29 '20 at 00:50
  • Thanks a lot - I'll look at it now. – Mel Feb 29 '20 at 02:06
  • Hey Mel, just updated further with the correct implementation of the Real Time updates from firestore as well (can remove the onSnapshot part to make it not real time) If this is the solution, please can I suggest possibly updating your question to make it a lot shorter and more concise so others viewing it might find the solution too, thanks - sorry again for the slow nature of responses – Tristan Trainer Feb 29 '20 at 11:37
  • Just looking back at your question the `WEBPACK_IMPORTED_MODULE_8` errors are due to not importing `firebase/auth` or similar modules. – Tristan Trainer Feb 29 '20 at 11:54
  • Slight tweak to the UseFirebase auth - need to initialize the AuthUser to the current user otherwise when you unmount/remount a new component there is a slight delay on the onAuthChanged triggering, which can lead to auth failing in `if(authUser)` sections – Tristan Trainer Feb 29 '20 at 19:03
  • Hi Mel, just wondering if the answer was useful? If so perhaps you could mark it as answered? :) – Tristan Trainer Mar 05 '20 at 13:08
  • This might be the longest stack overflow post I have ever seen. XD – heyitsalec Aug 27 '20 at 05:00

3 Answers3

28

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("name@email.com", "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

Tristan Trainer
  • 2,770
  • 2
  • 17
  • 33
  • 1
    thanks for this. I can't see how you've set this up. With the provider, I get an error that says createContext is not defined. That's because there is no consumer so far. Where have you put yours? – Mel Feb 05 '20 at 22:14
  • Hey sorry createContext is part of react, so import it at the top as { createContext } from 'react' I realised I forgot to show where the Firebase provider goes, I'll edit the answer – Tristan Trainer Feb 06 '20 at 00:06
  • I did import it in the provider, but it renders undefined. I think that's because there is no consumer for it – Mel Feb 06 '20 at 00:19
  • 1
    The consumer is the useContext() hook, but looking at your question again, it looks like you aren't exporting FirebaseContext from the file - that is why it can't find the context :) – Tristan Trainer Feb 06 '20 at 10:10
  • No - I have exported the FirebaseContext - but still get the error. There must be something else to this. I've downloaded another udemy on this and will update this post if I figure out what's gone wrong. – Mel Feb 06 '20 at 22:27
  • Thanks for the effort to help. If I figure out how to set this up, I'll share the answer here. Thanks just the same for the effort to help. – Mel Feb 09 '20 at 00:40
  • Hey Mel sorry been offline for a bit! I'll take a look tomorrow and try to recreate your situation then update you here :) – Tristan Trainer Feb 10 '20 at 23:04
  • The undefined error is because export const FirebaseContext = createContext(); needs to be replaced with export const FirebaseContext = React.createContext(); - but then that generates a bunch of new errors as follows – Mel Feb 12 '20 at 21:12
  • 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: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See react-invalid-hook-call for tips about how to debug and fix this problem. – Mel Feb 12 '20 at 21:13
  • Did you actually get your code to work? Which you tube videos did you find helpful in solving this problem? – Mel Feb 12 '20 at 21:13
  • When you say: TLDR: goto Use Firebase Auth Hook - do you mean this article (which is the first result returned on that search): https://medium.com/@johnwcassidy/firebase-authentication-hooks-and-context-d0e47395f402 or the donovan GitHub - which is an add-on? I tried Donavan's repo but that isn't solving this problem. – Mel Feb 14 '20 at 23:46
  • I can't make sense of your code. I've been trying for days. Please can you have a look to confirm that you have included everything it took to get it working. I'm up to episode 9 of the video series you recommended but haven't found the answer yet -is there an episode that is particularly helpful in solving this problem? – Mel Feb 15 '20 at 01:59
  • For others that might be trying to learn from this - it is full of errors. There may be clues that are helping people - clearly others have voted it up because they have found something useful in it - I have been working on the suggestions in this post for weeks - I'm dropping this effort and moving on - it is not useful – Mel Feb 27 '20 at 23:27
  • Hi @Tristan - thanks for this. I'm still trying to figure it out. I gave you the points to say thanks for your help. If I figure it out, I'll update this post and accept your answer. Very grateful for you sharing your thinking. I'm currently trying this approach https://usehooks.com/ with elements of your thought process – Mel Mar 05 '20 at 23:45
  • 1
    Hi @Mel thank you that's very kind, I hope it proves helpful in the end. Hooks and Firebase are both quite involved and it took me a long time to get my head around it and I may not have found the best solution even now, I might create a tutorial on my approach some time as it's easier to explain as you code it. – Tristan Trainer Mar 06 '20 at 10:54
  • I would read your tutorial if you decide to write one. I'm stuck. – Mel Mar 07 '20 at 23:36
  • Hey @Tristan, Just fyi if its of interest. I found this - so far it's working great for me. https://www.youtube.com/watch?v=Mi9aKDcpRYA – Mel Mar 21 '20 at 23:52
  • This looks great. What happens when you call the same hook in multiple places? Are multiple snapshot subscriptions set up? If so will this not have a performance impact? – Cathal Mac Donnacha Sep 28 '20 at 00:25
  • Hey @ReturnOfTheMac it will set up multiple subscriptions. This is intended as the snapshots are lightweight and you shouldn't have more than a couple per view/page (max I've had is 3). Definitely worth monitoring though if you do set up lots of subscriptions. Larger sites (needing a lot more subscriptions) probably should be looking at building a backend service for their own needs as Firebase is fairly basic. – Tristan Trainer Apr 19 '21 at 12:28
5

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.

fxdxpz
  • 1,969
  • 17
  • 29
  • I bought his book and followed his approach. I found the conditions approach did not actually work when implemented and that the auth protocol set out in that book failed to maintain status through component updates. I haven't found a way to use what is set out in that book. Thanks anyway for sharing your thoughts. – Mel Mar 07 '20 at 23:38
  • I am not sure what you mean. Have you tried my skeleton app with your firebase project? All conditions work as far as I can tell, coz I've been using it in at least 3 projects. – fxdxpz Mar 09 '20 at 10:00
2

Firebase is undefined because you are not importing it. First, its need to be firebase.firestore() as shown in the example on the docs you linked to https://github.com/CSFrequency/react-firebase-hooks/tree/master/firestore. Then you need to actually import firebase in the file, so import * as firebase from 'firebase'; as outlined in the firebase package readme https://www.npmjs.com/package/firebase

Josh Pittman
  • 7,024
  • 7
  • 38
  • 66
  • 1
    I am importing it in index.js – Mel Jan 30 '20 at 07:48
  • 1
    ReactDOM.render( , document.getElementById('root') ); – Mel Jan 30 '20 at 07:48
  • 1
    That's why the approach works with componentDidMount – Mel Jan 30 '20 at 07:48
  • 1
    The withAuth HOC is also wrapped in withFirebase. – Mel Jan 30 '20 at 07:51
  • 1
    The class helper I define in firebase.js is this.db (rather than firestore) so that's why the appoach in the class component is this.firebase.db rather than this.firebase.firestore – Mel Jan 30 '20 at 07:51
  • 1
    If you are passing your firebase instance to context then you need to pull it out of context and use it in the Dashboard2 component. The withAuth HOC might be wrapped with withFirebase but you are not referencing it anywhere so it is undefined. If you are saying that your instance of firebase is being passed into the Dashboard2 component, then reference it and use it. At the moment your build has no way of knowing what it is. Your approach is heavily over-engineered for no apparent reason, you can just add `import * as firebase from 'firebase'` like I explained in my answer. – Josh Pittman Jan 30 '20 at 09:42
  • 1
    If that logic were right, then it shouldn't have worked when I was using it in componentDidMount - but it did. I don't need another import statement in the Dash component - it's already included in the class, that is instantiated in the app – Mel Jan 30 '20 at 09:53
  • 1
    So what's the error you get when you make firebase lowercase and then import firebase into the file where you are using it? – Josh Pittman Jan 30 '20 at 10:53
  • 1
    It is imported. If it were not imported - it would not work correctly when I use it in a class component with componentDidMount. The error arises when I convert to react hooks with an attempt to use hooks to monitor state through the updates. Thanks for trying to the help. I'll post an answer here if I figure it out. – Mel Jan 30 '20 at 18:42
  • 3
    But your error says it is undefined. I'm helping you define it, and you are not telling me if the solution works or what the resulting error is. You always make it very hard to help you Mel. My point is that you are importing it in a file other than the dashboard2 component file, which is where you reference it, which is causing the error. Imorting something in index doesn't help your build understand what it in a completely different file. – Josh Pittman Jan 31 '20 at 05:15
  • 1
    Im grateful for your help. There might be something about a class component needing a different import statement than a function component. I haven't found any literature explaining that yet -but I'll keep looking. I'm certain firebase is imported in the file in which I'm trying to use it - when it's a class component - it works. I genuinely appreciate your efforts to help. I found another post (linked above) that is trying to solve the same problem and am now focusing on trying to implement that advice to solve this problem. Thanks again – Mel Feb 02 '20 at 00:13