DEV Community

Ben McMahen
Ben McMahen

Posted on • Originally published at benmcmahen.com on

Using Firebase with React Hooks

This tutorial demonstrates the use of hooks in your react application to better integrate firebase authentication and firestore data fetching. Before starting, it’s helpful to have a basic understanding of hooks, firebase authentication and firestore. By the end, we will be building some of the hooks found in our example application, Julienne.app.

Monitoring authentication

Using a combination of hooks and context makes it easy to access user sessions anywhere in your React application. We can store the user session in context, and pass that context to our child components. These components can then make use of hooks to access the session object.

First, create our context.

const userContext = React.createContext({
  user: null,
})

We supply our context with a default value containing a null session object. This will change when we use firebase to monitor changes to our session.

Next, we will create a hook that allows us to access our context.

export const useSession = () => {
  const { user } = useContext(userContext)
  return user
}

Finally, let’s create a hook that monitors the firebase authentication state. This hook will create state which uses a useState callback to determine whether a user session already exists. The callback is a useful way to initialize state with a value only upon the first mount of a componment.

Next, we use an effect which monitors authentication changes. When you trigger a login using one of the many firebase login methods (or you log out), the onChange function will be called with the current authentication state.

Finally, we return our authentication state.

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
}

We can then use this hook at the top level of our app and use our context provider to supply the user session to child components.

function App() {
  const { initializing, user } = useAuth()
  if (initializing) {
    return <div>Loading</div>
  }

  return (
    <userContext.Provider value={{ user }}> <UserProfile /> </userContext.Provider> )
}

Finally, within child components we can use our useSession hook to gain access to our user session.

function UserProfile() {
  const { user } = useSession() return <div>Hello, {user.displayName}</div>
}

To actually sign in or sign out, you really don’t need to use hooks at all. Simply call firebase.auth().signOut() or the various sign in methods in your event handlers.

Fetching a document

Hooks are useful for monitoring individual document queries using firestore. In this example, we want to fetch a recipe when provided an id. We’ll want to provide our components with error, loading, and recipe state.

function useRecipe(id) {
  // initialize our default state
  const [error, setError] = React.useState(false) const [loading, setLoading] = React.useState(true) const [recipe, setRecipe] = React.useState(null)
  // when the id attribute changes (including mount)
  // subscribe to the recipe document and update
  // our state when it changes.
  useEffect(
    () => {
      const unsubscribe = firebase.firestore().collection('recipes') .doc(id).onSnapshot( doc => { setLoading(false) setRecipe(doc) }, err => { setError(err) } )
      // returning the unsubscribe function will ensure that
      // we unsubscribe from document changes when our id
      // changes to a different value.
      return () => unsubscribe()
    },
    [id]
  )

  return {
    error,
    loading,
    recipe,
  }
}

Fetching a collection

Fetching a collection is very similar, but we instead subscribe to a collection of documents.

function useIngredients(id) {
  const [error, setError] = React.useState(false)
  const [loading, setLoading] = React.useState(true)
  const [ingredients, setIngredients] = React.useState([])

  useEffect(
    () => {
      const unsubscribe = firebase
        .firestore()
        .collection('recipes')
        .doc(id)
        .collection('ingredients') .onSnapshot( snapshot => { const ingredients = [] snapshot.forEach(doc => { ingredients.push(doc) }) setLoading(false) setIngredients(ingredients) }, err => { setError(err) } )

      return () => unsubscribe()
    },
    [id]
  )

  return {
    error,
    loading,
    ingredients,
  }
}

If you plan to use hooks with firebase throughout your application, I recommend checking outreact-firebase-hooks. It provides some useful helpers that allows us to reuse some of the logic that we wrote above.

For an example of a fully functioning app built with Firebase, React, and Typescript, check out Julienne.

(This is an article posted to my blog at benmcmahen.com. You can read it online by clicking here.)

Discussion (17)

Collapse
nikomontana profile image
Niko Montana • Edited

Good Tutorial!

Just one point - if you would have public accessible sites that doesn´t need auth. you will have to wait for the initialization. That would be a poor UX in my opinion.

I ended up just using the hook and check where necessary for initialization and user.

Anyway thanks!

Collapse
bmcmahen profile image
Ben McMahen Author

It should only show the loading on the initial rendering of the app. But i agree that you'd probably want to optimize this to render optimistically either using some local storage solution or simply rendering public pages before requiring the user object.

Collapse
nikomontana profile image
Niko Montana

Didn´t thought that you would response that fast :D

I have updated my comment!

Thread Thread
bmcmahen profile image
Ben McMahen Author

Awesome. Yep, I think your solution is definitely better UX. I'd advocate for a more fine-grained solution for any app mixing public / private routes.

Thread Thread
nikomontana profile image
Niko Montana

I must admit, my solution is pure nonsence. By implementing my logic I have to wait for every page to initialize firebase until I can access the user and in reality this is even worse UX.

So maybe a good conclusion:

Thanks for your post, you did a great job bud!

Thread Thread
bmcmahen profile image
Ben McMahen Author

Ah, bummer. Too bad. I do think there are strategies to improving this, though. Maybe a topic for another blog post...

Thread Thread
bulletninja profile image
Bulletninja

Maybe you can put that into a context, and try to get things from localStorage or sessionStorage, if they're not there, initialize, and reuse such context in all your pages

Collapse
tomasb profile image
TomasB

I think this:

      // returning the unsubscribe function will ensure that
      // we unsubscribe from document changes when our id
      // changes to a different value.
      return () => unsubscribe()

should be like that:

return unsubscribe;

Or am I missing something here?

Collapse
turpana profile image
your opinions, brought to you by AI

Great write-up! Looks like there's a typo, should be onAuthStateChanged, not onAuthStateChange. Thanks!

Collapse
bmcmahen profile image
Ben McMahen Author

Great catch. Thanks!

Collapse
technoplato profile image
Michael Lustig - halfjew22@gmail.com

Hey Ben, just learning about the Context API, so I’d like to do a quick sanity check.

In UserProfile I’m this example, assuming we don’t have any routine set up to redirect users to Login / Signup pages, shouldn’t we check that the user is not null and if it is, offer a Login button instead of the Username?

Thanks for your write up.

I may write something that corresponds to React Native land.

Collapse
kiwipedro profile image
kiwipedro

Thanks for this Ben, got me out of a hole!

Collapse
angiecortez profile image
angie.cortez

Hi Ben, did you tried to do a phone authentification with firebase and react hooks

Collapse
bmcmahen profile image
Ben McMahen Author

Hey Angie, sorry, I don't have any experience with phone authentication. From the looks of the docs it could get pretty complicated. But maybe I'll try adding phone-auth to one of my sample apps and get back to you.

Collapse
angiecortez profile image
angie.cortez

I hope you will help me, thank you

Collapse
marcosmartinim profile image
Martini

unsubscribe is not a function

Collapse
stoddabr profile image
stoddabr

Hi Martini,

Ben declared unsubscribe as a function variable. firebase.auth().onAuthStateChanged(*) returns firebase.Unsubscribe which is a function. This is sparsely mentioned in the documentation.