logo
Published on

Firebase authentication and Firestore database

Authors
  • avatar
    Name
    Lauren Jarrett
    Twitter

Introduction

When I first started learning React, I took a Udemy course by Andrew Mead which goes through and teaches you how to build an Expenses application using Firebase. The course was a great initial start to learn React and play around with Firebase, but it hasn't been updated since and naturally the application started to break. So I thought I would document the steps I recently took to update all the required libraries to get the application working again. This article focuses on just the steps I followed to update Firebase, jumping from the legacy and no longer supported version 4 to the newest web 9 api version.

The expenses application is hosted here here

Updating Firebase

The existing project used the legacy version 4.12 of Firebase, which is pretty similar syntactically to version 8 of the Web Api. So first things first, I grabbed the lastest version of Firebase SDK & saved it as a dependency into the package.json file a la:

npm install --save firebase@latest

Given my application already used Google OAuth Authentication & firestore database, I updated the new initalise App method and the appropriate firebase methods for Google Authentication and Firestore. Keeping my code tidy and small, this was all put in the aptly named firebase.js file to easily find and update later on.

After I created a project on Google, linked Firebase and ticked Authentication and Firestore, it gave me a list of credentials to use added to appropriate environment's environment variables file.

Authentication


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

const firebaseConfig = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.FIREBASE_APP_ID,
  measurementId: process.env.FIREBASE_MEASUREMENT_ID
};

const firebase = initializeApp(firebaseConfig);
const auth = getAuth(firebase);
const database = getFirestore(firebase);
const googleAuthProvider = new GoogleAuthProvider();

export { firebase, googleAuthProvider, auth, database as default };

I got an error when I have the database initialised via getFirestore before auth and had to switch them around. Digging into this quickly made sense when I read some methods for the realtime database in the sdk requiring authentication methods, hence it needs to be called first.

As I this app was deployed On Heroku for production and build locally with webpack, I had to use the webpack define plugin to be able to access process.env client side in my webpack.config.js file.


new webpack.DefinePlugin({
        'process.env.FIREBASE_API_KEY': JSON.stringify(process.env.FIREBASE_API_KEY),
        'process.env.FIREBASE_AUTH_DOMAIN': JSON.stringify(process.env.FIREBASE_AUTH_DOMAIN),
        'process.env.FIREBASE_PROJECT_ID': JSON.stringify(process.env.FIREBASE_PROJECT_ID),
        'process.env.FIREBASE_STORAGE_BUCKET': JSON.stringify(process.env.FIREBASE_STORAGE_BUCKET),
        'process.env.FIREBASE_MESSAGING_SENDER_ID': JSON.stringify(process.env.FIREBASE_MESSAGING_SENDER_ID),
        'process.env.FIREBASE_APP_ID': JSON.stringify(process.env.FIREBASE_APP_ID),
        'process.env.FIREBASE_MEASUREMENT_ID': JSON.stringify(process.env.FIREBASE_MEASUREMENT_ID),
        'process.env.TEST_VAR': JSON.stringify(process.env.TEST_VAR),
      }),

As this is a React Project using Redux, the Auth Actions needed to be updated as well. I imported the initialised googleAuthProvider and auth methods from firebase.js and imported the signInWithRedirect and signOut from the firebase/auth library.

import { googleAuthProvider, auth } from '../firebase/firebase';
import { signInWithRedirect, signOut } from "firebase/auth"

export const startLogin = () => {
  return () => {
    return signInWithRedirect(auth, googleAuthProvider);
  };
};

export const startLogout = () => {
  return () => {
    return signOut(auth)
  };

All that is now left to do is to update the login screen to capture the successful login and if so, direct the user to their dashboard screen. I also used the auth state firebase listener to capture when the user successfully logs out via firebase, they are logged out of the app.

import { auth } from './firebase/firebase';
import { getRedirectResult, onAuthStateChanged } from "firebase/auth";

getRedirectResult(auth).then((result) => {
  if (result && result.user) {
    const user = result.user
    store.dispatch(login(user.uid));
    store.dispatch(startSetExpenses()).then(() => {
      renderApp();
      if (history.location.pathname === '/') {
          history.push('/dashboard');
      }
    });
  } else {
    store.dispatch(logout());
    history.push('/');
    renderApp();
  }
});

onAuthStateChanged(auth, user => {
  if (!user) {
    store.dispatch(logout());
    history.push('/');
    renderApp();
  }
});

Looking at my Jest Tests to ensure that Login and Logout is working, the tests focused on checking if the actions are triggered when the buttons are clicked. Therefore we don't need to update them to cater for the new work flow and therefore are clear to run them and make sure all the tests pass.

Given how pretty quick this is, I tested it a few times locally to ensure that I was seeing users flow into the firebase users dashboard. The code needs to be better display arror messages to the user and I quickly add a TO DO to the code to ensure I don't forget this. However, Google oAuth is working, at least for an MVP! Part 1 done with a quick check-in to git.

Firestore database

Firestore on the other hand was significantly different SDK calls from the legacy verion so this update was going to take a little longer. Reading through the code, I need the updated methods to get, set, update documents and collections via firebase. Just as a side note, I chose set over addDoc as I always generate and pass in an ID, rather than let firebase generate the ID.

As everything was already initialised in firebase.js, I needed to update both the database action methods and the action tests to reflect the new methods.

As a high level, some of the main differences included the following. The new way to get the object data was via .data() rather than .val() and each method needed to be updated to remove the .ref() for a document or collection. Finally, the old .once() methods could also be removed. Looking at the before and after of the update method, the new web sdk is simpler, less code and more intuitive.

Before

return database.ref(`users/${uid}/expenses/${id}`).update(updates).then(() => {
      dispatch(editExpense(id, updates));
    });

After

return updateDoc(doc(database, 'users', `${uid}`, 'expenses', `${id}`), updates).then(() => {
      dispatch(editExpense(id, updates));
    })

Updated actions


export const startAddExpense = (expenseData = {}) => {
  return (dispatch, getState) => {
    const uid = getState().auth.uid;
    const {
      description = '', 
      note = '', 
      amount = 0, 
      createdAt = 0
    } = expenseData;

    const expense = { description, amount, note, createdAt };
    const id = uuidv4();
    
    return setDoc(doc(database, 'users', `${uid}`, 'expenses', `${id}`), expense).then(() => {
      dispatch(addExpense({
        id: id,
        ...expense
      }));
    });
  };
};


export const startRemoveExpense = ({ id }) => {
  return (dispatch, getState) => {
    const uid = getState().auth.uid;
    return deleteDoc(doc(database, 'users', `${uid}`, 'expenses', `${id}`)).then(() => {
      dispatch(removeExpense({ id }));
    })
  }
};


export const startEditExpense = (id, updates) => {
  return (dispatch, getState) => {
    const uid = getState().auth.uid;
    return updateDoc(doc(database, 'users', `${uid}`, 'expenses', `${id}`), updates).then(() => {
      dispatch(editExpense(id, updates));
    })
  };
};


export const startSetExpenses = () => {
   return (dispatch, getState) => {
     const uid = getState().auth.uid;
     return getDocs(collection(database, 'users', `${uid}`, 'expenses')).then((snapshot) => {
       const expenseArray = [];
       snapshot.docs.forEach((childSnapshot) => {
         expenseArray.push({
           id: childSnapshot.id,
           ...childSnapshot.data()
        });
      });
     dispatch(setExpenses(expenseArray));
   });
  };
 };

After testing these methods locally a few times to ensure data was being created, updated and deleted as per our expectations, I updated the expense database action tests to make sure there wasn't an area of manual testing I missed. As painful as this can be, I always find long term I never regret not adding or updating unit tests.

Once this was updated and all the tests working, firebase was successfully updated!

For more information on Firebase, check out the documentation

Thanks for following along!