How to Integrate Firebase Google Auth in your React App

How to Integrate Firebase Google Auth in your React App

·

14 min read

This blog will help you set up Google Authentication for your React application with the help of Firebase.

Google Authentication is one of the best and most robust authentication methods to authenticate your users. It is fast, Firebase also provides good UI/UX for the users and, most importantly, is secure.

This blog will help you integrate Google Authentication into your React Application and also show you some of the best React coding practices to make sure your code is well-structured and maintainable.

If you want to check the code and see how I have done it check the code here

This is what we are building: (Check the site here)

Create React App

The first step is to create a React application. I am using Vite for creation of my React App since it is fast and efficient and personally my favourite over create-react-app. Though you can choose your way to make the app it does not matter.

To start the app open your terminal and run
npm create vite@latest .

this will create the basic React app scaffold that we all know.

I have also added tailwind CSS for the app, the configuration of which can be learned from here.

Make sure you install all the dependencies using npm install inside the terminal and also install React Router DOM using npm i react-router-dom so that we can setup our routes for our app.

Firebase Auth Setup

To start this process we have to head towards the Firebase Console Website and add our project.

Follow the steps which include

  1. Enter your project name

  2. Enable Google Analytics for this project

  3. And Configure Google Analytics

After clicking on Create Project, Firebase will then create your project and then you are ready to configure your app.

Click on the web project button highlighted here in a blue box with an arrow

Register your app with any name you want here

It does ask us to set up Firebase Hosting. Since I am hosting the app on Vercel, I did not set it up though you can look into it. Skip the Add Firebase SDK part and directly go to the console.

In the Build menu situated in the left sidebar, click the Authentication option highlighted with a box and arrow below

From here, click on Get Started , go to sign in method and choose the Google method highlighted below

Now, enable the sign-in method, give it a public-facing name and choose your email ID for the support email and hit save

Now we are ready to add the Firebase authentication in our app

Firebase Auth Setup

Head on to the Projects Setting in Firebase and copy the configuration code

Go to the project again and install firebase inside your app.

Important: As you can see, I have hidden my configuration details from you all. This data is highly confidential and shall not be shared.

To protect this data, we must create a .env file for our app. Make sure this file is on the same level as the package.json file.

Now add the config information inside the env file

the env file should look like this:

VITE_API_KEY = "VITE_API_KEY" VITE_AUTH_DOMAIN = "VITE_AUTH_DOMAIN" VITE_PROJECT_ID = "VITE_PROJECT_ID" VITE_STORAGE_BUCKET = "VITE_STORAGE_BUCKET" VITE_MESSAGING_SENDER_ID = "VITE_MESSAGING_SENDER_ID" VITE_APP_ID = "VITE_APP_ID", VITE_MEASUREMENT_ID = "VITE_MEASUREMENT_ID"

Note: Vite handles env variables differently. To check how to set it up check out their docs here. For create-react-app, check this.

In you src folder, create config.js file for the firebase config inside of an independent folder named firebase

Therefore, your src/firebase/config.js file should look like this:

import { initializeApp } from "firebase/app";
import {getAuth, GoogleAuthProvider} from 'firebase/auth'

const firebaseConfig = {
  apiKey: import.meta.env.VITE_API_KEY, // Vite's way of accessing environment variables
  authDomain: import.meta.env.VITE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_APP_ID,
  measurementId: import.meta.env.VITE_MEASUREMENT_ID
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app)
const provider = new GoogleAuthProvider()
export {auth, provider}

Export the auth object and provider object. This will be used for login and logout functions of our app

Auth Context and Reducer set-up

Now that Firebase is set up, we are now going to see how do we handle the user login and logout alongside the user data. As user data is universally required and is an important piece of data, we are going to use useContext hook so that handling the data will be easy and so prop drilling will be required for the application

Now to handle the user data, which will change after user logs in or logs out, we are using useReducer hook to handle this efficiently

Now to start the code, create a context folder inside our src, where we must create a AuthContext.js file.

Import createContext and useReducer inside the app and initialise the AuthContextProvider logic like below

import { createContext , useReducer } from "react";

export const AuthContext = createContext()

export function authReducer(state , action){
    switch(action.type){
        case 'LOGIN': // To login, pass user data inside payload
            return { ...state, user: action.payload }
        case 'LOGOUT': // When logged out, user data shall be null
            return { ...state, user: null }
        case 'AUTH_IS_READY': // When the auth is ready, pass true to authIsReady
            return { ...state, user: action.payload, authIsReady: true }
        default:
            return state
    }
}

export default function AuthContextProvider({children}){
    const [state , dispatch] = useReducer(authReducer , {
        user :  null, // User data, initially null
        authIsReady : false // Is Auth API from Google ready?
    })
    // We are wrapping the App around the AuthContextProvider
    return (
        <AuthContext.Provider value={{...state , dispatch}}>
            {children}
        </AuthContext.Provider>
    )
}

The useReducer function returns us a state which holds the required value and a dispatch function that is used to update the said value. It is similar to state and setState values returned when we use useState() hook. The useReducer hook takes a reducer function which in this case is the authReducer function which gets called when we trigger the dispatch function and the initial state.

We are going to wrap AuthContextProvider component around our App.jsx app so that the entire app can access the dispatch function and user data with ease, accordingly, we have passed these values as the prop.

The authReducer functions handle 3 actions, LOGIN, LOGOUT and AUTH_IS_READY

The action parameter is what is passed from the dispatch function which the state parameter is the existing state value

  1. LOGIN: when the user login method is called, we call the dispatch function which should be an object having payload and type. The type being "LOGIN" and payload is the user data

  2. LOGOUT: when the user logs out, we call the dispatch function which should be an object having just the type. The type is "LOGOUT" and the user data is set to null.

  3. AUTH_IS_READY: Called when the Firebase Authentication is ready to use. Further discussed below

Now let's focus on what authIsReady is for.

As the app loads, reloads or we navigate to another page, the user data will point to null even though the user might be logged in. To avoid this, we are going to check whether a user is currently logged in or not, take the user data and signal the app that the auth is ready. For this we are going to use onAuthStateChanged method given by firebase.

Import auth from our config and useEffect and follow along:

import { createContext , useEffect , useReducer } from "react";
import {auth} from '../firebase/config'

function authReducer(){...}

export default function AuthContextProvider({children}){
    const [state , dispatch] = useReducer(authReducer , {
        user :  null,
        authIsReady : false
    })

    useEffect(() => {
        const unsub = auth.onAuthStateChanged((user) => {
            dispatch({
                type:'AUTH_IS_READY',
                payload: user // if the user is logged in, it holds user data, otherwise will be null
            })
            unsub()
        })
    } , [])
    return (
        <AuthContext.Provider value={{...state , dispatch}}>
            {children}
        </AuthContext.Provider>
    )
}

Now if you have guessed it, We will display the pages only when the authIsReady flag is true to avoid any issues.

Now let's see how we use the Context Provider inside our App.

useAuthContext hook

We all know by now how to handle our useContext and the provider with it. Know to handle any Errors with it, we have created a custom hook inside the /hook folder inside our /src folder.

import { AuthContext } from "../context/AuthContext"
import { useContext } from "react"

export const useAuthContext = () => {
  const context = useContext(AuthContext)
   // We can only access this context if we are inside the AuthContextProvider
  if(!context) {
    throw Error('useAuthContext must be used inside an AuthContextProvider')
  }

  return context
}

This is pretty straightforward. With this hook, we will access the user data and the dispatch function. We also need to check if the component that is trying to access the code is a child of our AuthContextProvider or not.

UseLogin() hook

Now comes the fun part. We are now going to handle the login functionality, which is the easiest part of our project

create the useLogin.js file inside the /hooks folder

As we are using the Google Auth, We can use the signInWithPopup method for authentication. The code looks like this:

import { useEffect, useState } from "react";
import { useAuthContext } from "./useAuthContext";
import { auth , provider } from "../firebase/config"
import { signInWithPopup} from "firebase/auth";

export default function useLogin() {
  const [isCancelled, setIsCancelled] = useState(false)
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);
  const { dispatch } = useAuthContext();

  const login = async () => {
    setIsPending(true);
    setError(null);
    try {
      const res = await signInWithPopup(auth , provider)
      dispatch({ type: "LOGIN", payload: res.user });
      if (!isCancelled) {
        setIsPending(false);
        setError(null);
      }
    } catch (err) {
      if (!isCancelled) {
        setError(err.message);
        setIsPending(false);
      }
    }
  };
  useEffect(() => {
    return () => setIsCancelled(true)
  }, [])

  return { login, isPending, error }
}

Now let me simplify the code for you.

For handling the login, we are also going to keep track of the pending state, error and cancellation of the login function when the component unmounts.

When login is called we are setting the isPending state as true and the error as null so clear any earlier error which is set.

login function is an async one since we are accessing a remote API. It calls the signInWithPopup function which takes the auth and provider as the arguments. It returns us a response that has the user data.

If everything goes fine (which 99.99% it does), we shall then call our dispatch function with the required argument, and object with

type : "LOGIN" and payload : res.user

Now if the user has not moved away from the app, the useEffect unsubscribe function is not called and the isCancelled flag is false, meaning we are not unmounted.

Now we will set our isPending and error states as false and null respectively

If any error occurs, we will pass the error message.

Now return the isPending, error states and login function from our hook.

UseLogout() hook

This hook's implementation is similar to the UseLogin hook. The only difference is the function we are calling.

We are simply going to call the signOut function and dispatch LOGOUT .

import { useEffect, useState } from "react";
import { useAuthContext } from "./useAuthContext";
import { auth } from "../firebase/config"
import { signOut } from "firebase/auth";

export const useLogout = () => {
    const [error, setError] = useState(null)
    const [isPending, setIsPending] = useState(false)
    const {dispatch} = useAuthContext()
    const [isCancelled, setIsCancelled] = useState(false)

    const logout = async() => {
        setIsPending(true)
        setError(null)
        try {
            await signOut(auth)
            dispatch({ type : 'LOGOUT'})
            if(!isCancelled){
                setIsPending(false)
                setError(null)
            }
        } catch(err){
            if(!isCancelled){
                setError(err.message)
                setIsPending(false)
            }
        }
    }
    useEffect(() => {
        return () => setIsCancelled(true)
    } , [])
    return {error, isPending, logout}
}

Here, we are passing the logout function, error message and the isPending state as before.

Integrating Login and Logout functions

Before we handle the routing, we will first handle the login and logout functionality and fix our AuthContextProvider to our app

Inside the main.jsx file (in CRA it is the index.jsx file), The App component will be declared under the AuthContextProvider .

Here is how it is to be implemented:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import AuthContextProvider from './context/AuthContext.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>

    <AuthContextProvider>
      <App />
    </AuthContextProvider>

  </React.StrictMode>
)

Now we can access the user state and the dispatch function inside the App component and their children.

Now, in our app, we are going to make two pages for demonstration. The landing page is going to be LandingPage which is going to be displayed to the user when he is not logged in, and HomePage which the user will see when the user is logged in.

LandingPage

The landing page has the Login in button. Here is the code for it

import React from 'react'
import useLogin from '../hooks/useLogin'

const LandingPage = () => {
  const {login, error, isPending} = useLogin()
  return (
    <div>
     <button 
        disabled = {isPending} 
        className='text-xl outline outline-1 py-2 px-4 rounded-md'
        onClick={login}
      >
        Login using Google
      </button>

      {error && <p className='text-red-700'>{error}</p>}
    </div>
  )
}

export default LandingPage

As you can see, because of all the heavy lifting we did on the user auth Context, the Reducer function and the custom hooks, the implementation of the login is super straightforward.

HomePage

The home page will be shown when the user is logged in and shall have the logout button. Implementation of which is also very easy.

import React from 'react'
import useLogout from '../hooks/useLogin'

const HomePage = () => {
  const {logout, error, isPending} = useLogout()
  return (
    <div>
     <button 
        disabled = {isPending} 
        className='text-xl outline outline-1 py-2 px-4 rounded-md'
        onClick={login}
      >
        Logout
      </button>

      {error && <p className='text-red-700'>{error}</p>}
    </div>
  )
}

export default LandingPage

Note: Further I have shifted the logout button inside the header component so if you are checking the code, go to /components/Header.jsx

Handling the Routes (the final part)

We come to the final task now.

We are going to set up BrowserRouter and also going to conditionally show the pages. To make it clear, If the user is logged in, then the user should only be allowed to visit HomePage. If the user tries to visit LandingPage, the user will be redirected to HomePage (this is just an example, you can have various routes that should be viewed by users based on their login status, however, the implementation is the same).

Inside the App.jsx folder, we will now handle the routing.

For this, you should create a Main.jsx page to handle the Outlet component in our app. This Outlet component will display the required page as requested.

The code for the same is here:

import React from 'react'
import { Outlet } from 'react-router-dom'
import Header from '../components/Header'

const Main = () => {
  return (
    <div className='h-[100vh]'>
      <Header/>
        <Outlet/>
    </div>
  )
}

export default Main

Import the useAuthContext from our hooks and createBrowserRouter and RouterProvider.

The createBrowserRouter consists of an array of Route data that holds the required path, the element to be displayed and the children of the given path.

The RouterProvider is the component that displays the components as required by the URL path.

We now use our important property isAuthReady. The RouterProvider will only be rendered only when the isAuthReady is true. Meaning, that we will display the pages only when the auth is ready and we have gotten the idea of whether there is a user already logged in or not. This confirmation will help us avoid any unexpected behaviour.

Now, let's discuss the most important part, displaying the users by logged-in status. Now if you visit the react-router-dom docs to get more information about this, they suggest using the loader function for this.

The code is simple for the same, for LandingPage, if the user object exists (meaning the user is logged in) we will redirect the user to HomePage, else we will stay on the LandingPage.

For the HomePage, if the logged-in user data does not exist, we will simply redirect to LandingPage, else we stay.

import './App.css'
import {createBrowserRouter , RouterProvider, redirect} from 'react-router-dom'
import Main from './layout/Main'
import LandingPage from './pages/LandingPage'
import HomePage from './pages/HomePage'
import { useAuthContext } from './hooks/useAuthContext'



function App() {
  const {authIsReady , user} = useAuthContext()
  const router = createBrowserRouter([
    {
      path : "/",
      element : <Main/>,
      errorElement : <h1>Oops , Something went wrong!</h1> ,
      children : [
        {
          index : true,
          element : <LandingPage/>,
          loader : () => {
            if(user){
              return redirect('/home')
            }
            return null
          }

        },
        {
          path : 'home',
          element : <HomePage/>,
          loader : () => {
            if(!user){
              return redirect('/')
            }
            return null
          }
        }
      ]
    }
  ])
  return (
    <>
      {authIsReady && (
        <RouterProvider router={router} />
      )}
    </>
  )
}

export default App

Now you know how to handle Google authentication in your React app. You can further create various routes and pages as you want. From here, accessing user data and logging in or out of the user is very straightforward.

If you want to check the app and how is it functioning, check the app here.

If you have any doubts or have any suggestions, you can contact me on twitter or LinkedIn. If you want to check the code you can see the GitHub repo too.

Lastly thank you for stopping by and reading my Blog. If you liked this blog, please like it and share it with people who might find it helpful, it is very helpful.

That's all folks! See you next time!