DEV Community

Cover image for React + Firebase: A Simple Context-based Authentication Provider
Denny Christochowitz
Denny Christochowitz

Posted on

React + Firebase: A Simple Context-based Authentication Provider

This post showcases a quick and easy way to make a Firebase-authenticated user available throughout your React web app.

We are using here plain React with Typescript, and no extra state management library like Redux involved.

Firebase offers us to register a callback that gets called every time a user is authenticated or signed out to get notified about the current authentication situation.

import firebase from "firebase/app";

firebase.auth().onAuthStateChanged((user) => {
  if (user) {
    console.log("authenticated", user);
  } else {
    console.log("signed out");
  }
});
Enter fullscreen mode Exit fullscreen mode

We thus could implement a React component that is interested in the authenticated user quite straightforward like this:

import * as React from "react";
import firebase from "firebase/app";

function CurrentUser() {
  const [user, setUser] = React.useState<firebase.User | null>(null);

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

  return <div>{user?.displayName || "unauthenticated"}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Our React component facilitates React.useEffect to register the Firebase onAuthStateChanged callback once after it was mounted. The effect returns the unsubscribe callback from onAuthStateChanged, ensuring that we don't run in any memory leaks.

Additionally, we have a state for the current user which's setter happens to match the callback signature perfectly.

This works just fine if only a single component in your React app is interested in the authentication state. Duplicating the state and effect for each other component would be cumbersome.

But more importantly, this approach works only for permanent (not conditionally rendered) components in our app's render tree, since otherwise, they might miss the initial authentication state because onAuthStateChanged only notifies changes.

One way to tackle this is to provide the authentication state globally utilizing a React context and companion hook. Let's start with the context first:

// FirebaseAuthContext.tsx
import * as React from "react";
import firebase from "firebase/app";

type User = firebase.User | null;
type ContextState = { user: User };

const FirebaseAuthContext =
  React.createContext<ContextState | undefined>(undefined);

const FirebaseAuthProvider: React.FC = ({ children }) => {
  const [user, setUser] = React.useState<User>(null);
  const value = { user };

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

  return (
    <FirebaseAuthContext.Provider value={value}>
      {children}
    </FirebaseAuthContext.Provider>
  );
};

export { FirebaseAuthProvider };
Enter fullscreen mode Exit fullscreen mode

Few things to note here:

  • User is a type alias for the authenticated Firebase user returned by onAuthStateChanged. The callback is called with null if no user is authenticated.
  • ContextState is a type alias for the value provided by our context FirebaseAuthContext.
  • We do not expose FirebaseAuthContext directly. Instead we expose FirebaseAuthProvider which encapsulates FirebaseAuthContext.Provider and a onAuthStateChanged subscription. It's quite similar to the CurrentUser implementation above.

Now let's define a simple hook that gives components interested in the authenticated user access to it:

// FirebaseAuthContext.tsx
// ...

function useFirebaseAuth() {
  const context = React.useContext(FirebaseAuthContext);
  if (context === undefined) {
    throw new Error(
      "useFirebaseAuth must be used within a FirebaseAuthProvider"
    );
  }
  return context.user;
}

export { FirebaseAuthProvider, useFirebaseAuth };
Enter fullscreen mode Exit fullscreen mode

Our hook useFirebaseAuth simply facilitates React.useContext to access the previously defined context. We explicitly check for undefined to catch possible misuses as early as possible.

FirebaseAuthProvider usually is instantiated only once in an App, typically near the root in order to give all components below the opportunity to access the user via useFirebaseAuth. Here's a simple (constrived) example:

// example.ts
import * as React from "react";
import { FirebaseAuthProvider, useFirebaseAuth } from "./FirebaseAuthContext";

// ...initialize firebase

function App() {
  return (
    <FirebaseAuthProvider>
      <UserName />
      <UserEmail />
    </FirebaseAuthProvider>
  );
}

function UserName() {
  const user = useFirebaseAuth();
  return <div>{user?.displayName || "unauthenticated"}</div>;
}

function UserEmail() {
  const user = useFirebaseAuth();
  return <div>{user?.email || "-"}</div>;
}
Enter fullscreen mode Exit fullscreen mode

A few things to note:

  • Firebase initialization is left out for the sake of brevity. You can check it out here if you haven't already.
  • The hook can be used by any component below FirebaseAuthProvider regardless of nesting level.
  • Every notification of onAuthStateChange triggers a re-render.
  • If your app manages state with Redux or a similar library, you may be better off handling the authentication state there as well.

I've found this approach very simple to implement and apply. It is based on Kent C. Dodds excellent blog post "How to use React Context effectively". You should definitely go and check it out for a more detailed description and some more background info.

Thanks for reading 🤗

If you liked it and don't mind, give it a ❤️

Take care & happy coding 🙌

Photo by Markus Spiske on Unsplash

Top comments (14)

Collapse
 
patryksztuczka profile image
Patryk Sztuczka • Edited

From the point of view of the person who does it for the first time, it's the most accessible learning material I have found. It's worth noting, for people who, like me read this article over a year later, that in version 9 of firebase, the onAuthStateChanged function takes the Auth instance as the first parameter.

Collapse
 
tomzacchia profile image
tomzacchia

What's the benefit of implementing this Context in contrast to using the useAuthState hook that comes with the firebase dependency? github.com/CSFrequency/react-fireb...

Collapse
 
dchowitz profile image
Denny Christochowitz

I was unaware of this package. The benefit from my point of view is not having an extra dependency for this simple topic, but this is certainly a debatable point. I could imagine that there is no benefit from a functional perspective, but I haven't yet looked into the package you mentioned.

Anyway, thanks for pointing out the hooks package! After working on a small Firebase app, I wrote this post where this simple hook described here came up almost incidentally.

Collapse
 
tomzacchia profile image
tomzacchia

You certainly have a good point about having extra dependencies. Admittedly I haven't explored the react-firebase-hooks in detail (i.e firestore, storage etc..). However I will say that a benefit to the Context approach is containing other side effects when the Auth state does change, for example solving the problem where auth.currentUser is null when the app starts and private routes

Collapse
 
mahamoti1129 profile image
Mahamoti1129

Just so you know, there's a reddit user reposting this content as his own on multiple subs:

reddit.com/r/reactjs/comments/nr8i...

Collapse
 
dchowitz profile image
Denny Christochowitz

Oh no, he spammed a lot of sub-reddits with my post. I'll report him.

Collapse
 
dchowitz profile image
Denny Christochowitz

Thx for letting me know. Seems that the reddit post has been deleted in the meantime...

Collapse
 
digizeph profile image
Mingwei Zhang

Thank you for the post. Very clear and helpful!

Collapse
 
theooliveira profile image
Theo Oliveira • Edited

for some reason i get a warning saying i am using a hook inside a useEffect. i am using typescript.

Collapse
 
willuk profile image
Will Halling

Thanks a lot @dchowitz but I need to use the user.uid in an API call before the render in useEffect() - how can I do this?

Collapse
 
dchowitz profile image
Denny Christochowitz

Hi Will, there are different options here. You could make the useEffect which wraps the API call dependent on user.id like this:

React.useEffect(() => {
  if (user.id) {
    // perform API call
  }
}, [user.id]);
Enter fullscreen mode Exit fullscreen mode

This calls the effect every time the user.id changes... Hope this helps!

Collapse
 
shreeshivpatel profile image
Shreeshiv Patel

I don't know why but for some strange reason, I have to login again after I refresh. Is anyone facing the same issue..

Collapse
 
shreeshivpatel profile image
Shreeshiv Patel

It looks like my onAuthStateChanged not calling after signIn or signUp. May I know possible reason for the same.

Collapse
 
shreeshivpatel profile image
Shreeshiv Patel

What is purpose of
(React.createContext < ContextState) | (undefined > undefined);