Using Firebase with React Hooks

March 30, 2019

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().onAuthStateChange(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 out react-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.

Contact me on Twitter with any thoughts or questions.
I'd love to hear from you.

Subscribe via RSS
Ben
I'm a web and mobile developer based in British Columbia, Canada. I love creating beautiful, fun, and interactive tools to help people learn.