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 runnpm 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
Enter your project name
Enable Google Analytics for this project
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
LOGIN: when the user login method is called, we call the
dispatch
function which should be an object havingpayload
andtype
. The type being "LOGIN" andpayload
is the user dataLOGOUT: when the user logs out, we call the
dispatch
function which should be an object having just thetype
. The type is "LOGOUT" and the user data is set to null.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!