• 2019-06-08
  • React
  • Firebase

Simple User Authentication In React Using Firebase

Building a simple React Hook using Firebase that manages all app authentication

My current side project requires authorized users to create events. This time around I wanted to try out Google's Firebase β€” after a bit of research, docs, and playing around with the examples, I was amazed at how simple authentication can be!

Firebase exposes a series of APIs that handle all user authentication - we can simply wrap the APIs with a single, reusable component.

In this post we will be creating a single React hook that uses Firebase to easily manage user authentication that you can use throughout your app.

Here's how it can be used:

export default function App() {
  const context = useAuth()
  const routes = context.user ? authorizedRoutes : noUserRoutes

  return (
    <Layout>
      <Router>
        <Switch>
          {routes.map(r => (
            <Route key={r.path} {...r} />
          ))}
        </Switch>
      </Router>
    </Layout>
  )
}

Instead of publishing the final code as a module, I just created a gist which you can find on Github. Simply copy and paste it into your app, and edit the code based on your needs!

We'll go over the following:

  1. Setting up our component
  2. Initiating Firebase
  3. Implementing signup, signin, and sendResetPasswordEmail methods
  4. Caveats and gotchas

Setting up our component

Because authentication is a value used by components throughout the app, we will store the user in our component state, and expose the user object to all nested components in our app via React's Context API.

The only state that our component will manage are:

  • isInitiallyLoading: on initial page load, we'll need to fetch the current authentication status from Firebase.
  • isLoading: a flag for when we make API calls to Firebase to disable further form submissions/disable buttons.
  • user: the user

In a file, use-auth.js, let's set up our component with React Context as well as a React.useReducer for state management:

import React from 'react'

// Create aliases for our action types - this helps with
// autocomplete and allows us to not have to type in the
// actual string when dispatching updates.
AuthProvider.actions = {
  setUser: 'SET_USER',
  toggleLoading: 'TOGGLE_LOADING',
}

const reducer = (state, action) => {
  switch (action.type) {
    //
    // We'll add different cases here, later.
    //
    default:
      throw new Error(`No case for type ${action.type} found.`)
  }
}

const AuthContext = React.createContext(undefined)

export function AuthProvider({ initialUser, children }) {
  const [state, dispatch] = React.useReducer(reducer, {
    isInitiallyLoading: true,
    isLoading: false,
    user: null,
  })

  //
  // We'll add all our methods here, later.
  //

  const value = {
    user: initialUser || state.user,
    isLoading: state.isLoading,
  }

  // On our initial page load, we'll be fetching data regarding the user
  // from Firebase. During this time, we'll simply show a full page loader.
  return state.isInitiallyLoading ? (
    <FullPageLoading />
  ) : (
    <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
  )
}

Okay, now that we have our initial skeleton set up let's move onto incorporating Firebase into our component.

Initiating Firebase

Go to your Firebase Console and click '+ New Project'. Select your project name (if you don't have a project created, go to Google Cloud Console and create a new project.

When you've created your new Firebase project, you will be given credentials like this:

{
  apiKey: <API_KEY>,
  authDomain: 'my-new-project-123.firebaseapp.com',
  databaseURL: 'https://my-new-project-123.firebaseio.com',
  projectId: 'my-new-project-123',
  storageBucket: 'my-new-project-123-.appspot.com',
  messagingSenderId: <SENDER_ID>,
  appId: <APP_ID>,
}

We'll need to load these credentials into our component, so I just placed them inside a folder, credentials, and exported the values as a variable:

In src/credentials/firebase-config.js or where ever you'd like to keep the credentials:

const firebaseConfig = {
  apiKey: <API_KEY>,
  authDomain: 'my-new-project-123.firebaseapp.com',
  databaseURL: 'https://my-new-project-123.firebaseio.com',
  projectId: 'my-new-project-123',
  storageBucket: 'my-new-project-123-.appspot.com',
  messagingSenderId: <SENDER_ID>,
  appId: <APP_ID>,
}

export default firebaseConfig

Let's also install Firebase into our app:

yarn add firebase

or

npm install firebase

Back to our main use-auth.js, we'll update our imports to include Firebase.

import React from 'react'
import firebaseConfig from '../path/to/credentials/firebase-config'
import firebase from 'firebase/app'
import 'firebase/auth'

Now, when our component initially mounts, we'll want to setup what Firebase calls an onAuthStateChanged listener. What this does is sets up a listener that fires any time authorization changes within our app. The naming is nice and descriptive πŸ˜„. Let's set up a React.useEffect that is called only on our component's initial mount:

React.useEffect(() => {
  // Setup Firebase authentication state observer and get user data.
  if (!firebase.apps.length) {
    firebase.initializeApp(firebaseConfig)
  }

  // Whenever we sign in or out users, authStateChanged callback will be triggered.
  firebase.auth().onAuthStateChanged(function(user) {
    if (user) {
      // User is signed in.
      dispatch({
        type: AuthProvider.actions.setUser,
        payload: {
          user,
        },
      })
    } else {
      // User is signed out.
      dispatch({
        type: AuthProvider.actions.setUser,
        payload: {
          user: null,
        },
      })
    }
  })
}, [])

Let's go up to our reducer and write the logic for our AuthProvider.actions.setUser. This will update our user state based on whatever value authStateChanged returns:

const reducer = (state, action) => {
  switch (action.type) {
    case AuthProvider.actions.setUser:
      return {
        user: action.payload.user,
        isInitiallyLoading: false,
        isLoading: false,
      }
    default:
      throw new Error(`No case for type ${action.type} found.`)
  }
}

We're actually almost done here! Now, on a full page refresh, we'll be displaying a full page loading view, then initiating Firebase's onAuthStateChanged listener, which will return to a valid or invalid user object. We then take the returned user value and dispatch a state update based on whatever that value is!

Implementing signup, signin, and sendResetPasswordEmail methods

This part is super simple! Firebase exposes a series of APIs for us to authenticate, register a new user, send reset password emails, and more. They literally handle everything πŸ™.

In this post we will only be using the following APIs:

  • createUserWithEmailAndPassword
  • signInWithEmailAndPassword
  • signOut
  • sendPasswordResetEmail

We will simply wrap those APIs in our own functions, adding just a little bit of extra logic to update state. Let's go through each method:

createUserWithEmailAndPassword

const signup = (email, password, displayName) => {
  toggleLoading(true)

  let user: any

  firebase
    .auth()
    .createUserWithEmailAndPassword(email, password)
    .then(() => {
      user = firebase.auth().currentUser
      user.sendEmailVerification()
    })
    .then(() => {
      user.updateProfile({
        displayName,
      })
    })
    .then(() => {
      toggleLoading(false)
    })
    .catch(function(error) {
      // Handle Errors here - you can dispatch some action here that
      // displays an error message.
      const errorCode = error.code
      const errorMessage = error.message

      console.log('errorCode', errorCode, 'errorMessage', errorMessage)
      toggleLoading(false)
    })
}

Firebase's createUserWithEmailAndPassword API only creates a user with an email and password; however, when signing up new users we also want to add their names.

In our case, once createUserWithEmailAndPassword finishes, we'll take the updated user and additionally call updateProfile, updating the user's displayName.

signInWithEmailAndPassword

const signin = (email, password) => {
  toggleLoading(true)

  firebase
    .auth()
    .signInWithEmailAndPassword(email, password)
    .then(() => {
      toggleLoading(false)
    })
    .catch(function(error) {
      // Handle Errors here.
      const errorCode = error.code
      const errorMessage = error.message

      console.log('errorCode', errorCode, 'errorMessage', errorMessage)
      toggleLoading(false)
    })
}

signOut

const signout = () => {
  toggleLoading(true)

  firebase
    .auth()
    .signOut()
    .then(function() {
      // Sign-out successful.
      toggleLoading(false)
    })
    .catch(function(error) {
      // An error happened.
      toggleLoading(false)
    })
}

sendPasswordResetEmail

const sendResetPasswordEmail = email => {
  toggleLoading(true)

  firebase
    .auth()
    .sendPasswordResetEmail(email)
    .then(function() {
      // Email sent.
      toggleLoading(true)
    })
    .catch(function(error) {
      // An error happened.
      toggleLoading(false)
      console.log('error', error)
    })
}

We'll add these new methods to our Context value.

// const value = {
//   user: initialUser || state.user,
//   isLoading: state.isLoading,
// }

const value = {
  user: initialUser || state.user,
  isLoading: state.isLoading,
  signup,
  signin,
  signout,
  sendResetPasswordEmail,
}

Okay great! Everything is pretty much done. Let's wrap our entire component with a simple hook β€”Β this is what users of our API will import and use:

export default function useAuth() {
  const context = React.useContext(AuthContext)

  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider')
  }

  return context
}

The last step is to wrap our App within the AuthProvider that we created. This will allow all nested child components to have access to our Context. In index.js:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

import { AuthProvider } from './path/to/use-auth'

ReactDOM.render(
  <AuthProvider>
    <App />
  </AuthProvider>,
  document.getElementById('root')
)

Booom. Done! πŸŽ‰ We've created a component which uses React's Context and Hooks to interface with Firebase's APIs to provide any components within our app access to user authentication!

Say we are in our <Signup /> component and we want to handle signing users up. We simply do the following:

import React from 'react'
import useAuth from '../path/to/use-auth'

export default function Signup() {
  const { isLoading, signup } = useAuth()

  return (
    <Form onSubmit={signup}>
      <Label htmlFor="email" />
      <Input />
      <Label htmlFor="password" />
      <Input />
      <Button disabled={isLoading} type="submit">
        {isLoading ? 'Signing up...' : 'Sign up'}
      </Button>
    </Form>
  )
}

Caveats and gotchas

While building out my project, I noticed that when we initially call createUserWithEmailAndPassword during signup, our user is created and onAuthStateChanged callback is triggered prior to user.updateProfile completes. I figured it has to do with updateProfile being an async API.

So when we first signup a user and our user state is updated, the user's displayName property will still be null.

To combat this issue, we can edit our onAuthStateChanged callback and signup code and do a little workout like this:

// Set a flag to determine if onAuthStateChange is triggered from signup.
const signingInSoDontDispatchOnAuthStateChange = React.useRef(false)

React.useEffect(() => {
  // ...

  firebase.auth().onAuthStateChanged(function(user) {
    if (user) {
      // If our onAuthStateChanged is called after a user signs up, we
      // won't dispatch any updates here. Instead, we'll dispatch the
      // update in our signup method.
      if (signingInSoDontDispatchOnAuthStateChange.current) {
        signingInSoDontDispatchOnAuthStateChange.current = false
        return
      }

      dispatch({
        type: AuthProvider.actions.setUser,
        payload: {
          user,
        },
      })
    } else {
      // ...
    }
  })
}, [])

const signup = (email, password, displayName) => {
  signingInSoDontDispatchOnAuthStateChange.current = true

  toggleLoading(true)

  let user: any

  firebase
    .auth()
    .createUserWithEmailAndPassword(email, password)
    .then(() => {
      user = firebase.auth().currentUser
      user.sendEmailVerification()
    })
    .then(() => {
      user.updateProfile({
        displayName,
      })
    })
    .then(() => {
      toggleLoading(false)
      // Set user with displayName here because user.updateProfile
      // is async and our onAuthStateChanged listener will fire
      // before the user is updated. When that happens, user.displayName
      // value will be null. Now, our user state will be updated with
      // a valid displayName.
      // Reference: https://github.com/firebase/firebaseui-web/issues/36
      const updatedUserWithDisplayName = {
        ...user,
        displayName,
      }

      dispatch({
        type: AuthProvider.actions.setUser,
        payload: {
          user: updatedUserWithDisplayName,
        },
      })
    })
    .catch(function(error) {
      // ...
    })
}

And that's it! If you have any questions or have found an alternative way of simplifying user authentication in your React app, please feel free to reach out to me on Twitter. Until then, have fun building your UI! ✌️

Previous
  • Javascript
  • Three.js
  • WebGL

Building An Interactive WebGL/Three.js Globe

Up Next
  • Project Management
  • Design
  • Development

Modernizing and Redefining Healthcare Services