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.)
Top comments (17)
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!
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.
Didn´t thought that you would response that fast :D
I have updated my comment!
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.
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!
Ah, bummer. Too bad. I do think there are strategies to improving this, though. Maybe a topic for another blog post...
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
I think this:
should be like that:
Or am I missing something here?
Great write-up! Looks like there's a typo, should be
onAuthStateChanged
, notonAuthStateChange
. Thanks!Great catch. Thanks!
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.
Thanks for this Ben, got me out of a hole!
Hi Ben, did you tried to do a phone authentification with firebase and react hooks
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.
I hope you will help me, thank you
unsubscribe is not a function
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.