Unrelated photo by Pathum Danthanarayana on Unsplash
Dipped my toe into Google’s Firebase to explore more on serverless.
In particular, I integrated Firebase Authentication and Firestore (their cloud database) into my frontend project (built with React and Redux).
The simple features I set out to build:
- user is able to sign up
- user is able to login
- user is able to view profile
- user is able to update profile
Here’s a quick starter’s guide:
SETUP
go to Firebase dashboard, create your app. and setup the services you need (authentication, firestore, etc). remember to get your project ID.
CONFIG
create a config file below:
// import firebase and necessary services.import firebase from “firebase/app”;import “firebase/auth”;//your API keysimport “firebase/firestore”;const firebaseConfig = {apiKey: “…”,authDomain: “….firebaseapp.com”,databaseURL: “….firebaseio.com”,storageBucket: ‘….appspot.com’,projectId: “…”,//initialise firebase with your keys/ids};firebase.initializeApp(firebaseConfig);// export to use easily in components/redux actionsexport default firebase;
SIGNUP ON AUTH
in our signup function, there are two main things we need to do:
- create user with Firebase Authentication
- create user with Firestore
Note that you need to create user with both, otherwise even though user can signup/login via Auth, you don’t have a user document in our Firestore database.
Here, we start with the signup function to create user on Auth using email/password (Auth has integration with social media login as well).
firebase.auth().createUserWithEmailAndPassword(email, password)
We will connect with Firestore later in the guide.
LOGIN
Just need to call this function and pass the right arguments:
firebase.auth().signInWithEmailAndPassword(email, password)
AUTH LISTENER
We are setting up an Auth Listener to check if user is logged in (so that we can greet the user, allow user access to certain routes, etc). With the Auth Listener, we can dispatch an action to update user’s state.
We can call the authListener function below using componentDidMount or useEffect hook at App.js:
const authListener = () => {firebase.auth().onAuthStateChanged((user) => {if (user) {dispatch(allActions.userActions.setUserAuth(user.displayName))} else {dispatch(allActions.userActions.setUserAuth(null))}})}// setUserAuth is our redux action to update user’s state to greet user
ADD VALIDATION WHEN SIGNING UP
in our signup function, before createUserWithEmailAndPassword is executed, we can add email / password / ToS checkbox validation :
const phone = document.getElementById(‘inputphone’).value;const tos = document.getElementById(‘tos-privacy-checkbox’).checked;if (phone.length < 4) {alert(‘Please enter a valid phone number.’);return;}// if you prefer to style your alert/error message using an error message state:if (!tos) {return dispatch({type: ERROR_MESSAGE,payload: “Please accept Terms and Privacy to proceed”})}
ADD PROFILE TO AUTH WHEN SIGNING UP
Firebase Auth has basic user properties (displayName, phoneNumber) that we can populate for our use.
We need to chain auth’s updateProfile after createUserWithEmailAndPassword.
Else, our user in Auth will only have email and password.
firebase.auth().createUserWithEmailAndPassword(email, password).then(function(result) {return result.user.updateProfile({displayName: name,phoneNumber: phone})})
REDIRECT AFTER LOGIN / IF LOGIN
after user login, we want to redirect user to somewhere. Here we are using react-router-dom’s Redirect:
in Login component, add:
if (user) { //refer to user state that we populate upon signup or via auth listenerreturn(<Redirect to=”/shop” />)}
the above logic will apply if user tries to go to /login while logged-in as well.
do the same for our Signup component.
REDIRECT AFTER SIGNUP
after Signup success, Firebase should automatically login user, and user will be redirected since auth listener will update user state.
Issue faced:
Initially, we can see the user’s state updated only after manual refresh. For some reason, our auth listener doesn’t trigger right away after user signs up, and still sets our user’s state null. It could be because we are not using the useEffect hook correctly.
Anyway, as a hack/manual fix, in our Signup function, after createUserWithEmailAndPassword(), we add the following to manually update user’s state.
.then(() => dispatch({type: SET_USER_AUTH,payload: name}))
SIGN UP SUCCESS MESSAGE
after signup success in signup action, after createUserWithEmailAndPassword, add:
.then(alert(“sign up success yay!”))
LOGOUT USER
pretty straight forward function.
const logOutUser = () => (dispatch) => {firebase.auth().signOut().then(function() {// if you want to do something, else leave blank}).catch(function(error) {alert(error)});}
CONNECTING TO DB (FIRESTORE)
We have only touched Authentication thus far. We will need to connect to Firestore (our serverless DB of choice here) if we want to store more user data e.g. wishlist, purchase history, etc.
Note that Firestore uses NoSQL. The recommended way is for each user to be a document in a collection called “users”
In the same config file, add:
import “firebase/firestore”;firebase.firestore();
Important: on Firestore dashboard, update Firestore security rule (if it was locked down). For our testing purpose, we can set “allow read, write: if true”;
Read more here.
UPDATE DATABASE WHEN USER SIGNUP
Note: we need to update user to Firestore/DB when user sign up. Else, user data will stay at Auth only.
One way is to use Cloud Function that will be event-triggered to update Firestore on the background.
Another is to chain it in the userSignup function after createUserWithEmailAndPassword.
Initially, we chained it at the end of the signup function:
//FAILED attempt:firebase.auth().createUserWithEmailAndPassword(email, password)//code omitted for update state//alert success//check error.then(function() {firebase.firestore().collection(‘users’).add({email: email,name: name,phone: phone,})})
With this approach, we encountered an issue: UID of Authentication is different from UID of Firestore user’s document title;
Due to the async nature and latency of createUserWithEmailAndPassword, we were not able to directly access firebase.auth().currentUser.uid during user sign up process as it returns null.
Hence even though we managed to create a user in Firestore db, we could not use auth’s UID as document ID, i.e. the same user has different UID in Auth and in Firestore, which is not ideal.
SOLUTION:
previously, we add user to DB only at the end of the chain of asynchronous function.
our fix is to add user to DB immediately while we have access to Auth’s UID in the object returned by createUserWithEmailAndPassword:
const userSignupSubmit = () => (dispatch) => {// omitted code for field validations// action 1firebase.auth().createUserWithEmailAndPassword(email, password).then(function(result) {// action 2firebase.firestore().collection(‘users’).doc(result.user.uid).set({email: email,name: name,phone: phone,});// action 3return result.user.updateProfile({displayName: name,phoneNumber: phone})})}
To summarise, here are the 3 main actions in our signup function:
- action 1: create user with firebase auth
- action 2 (async): when we get the returned object (result), we now have access to result.user.uid which we can use to create a new user document
- action 3 (async) with access to the returned object from action 1, we can update some of its properties, ensuring that our Auth user has displayName and phoneNumber. This step is optional if you are not using these properties from Auth.
GET USER PROFILE
using auth’s UID to fetch profile and update profile state from firestore database:const getProfile = () => (dispatch) => {const userUid = firebase.auth().currentUser.uid;firebase.firestore().collection(‘users’).doc(userUid).get().then(function(user) {if (user.exists) {const email = user.data().email;const name = user.data().nameconst phone = user.data().phonereturn dispatch({type: READ_PROFILE,payload: {email: email,name: name,phone: phone}})} else {console.log(“user doesn’t exist”) //handle later}})}
UPDATE USER PROFILE
remember to update both the Auth and Firestore DB.
const updateProfile = () => (dispatch) => {…const user = firebase.auth().currentUser;user.updateProfile({ // updating profile in AUTHdisplayName: name,phoneNumber: phone}).then(function() { // updating Firestore DBfirebase.firestore().collection(‘users’).doc(user.uid).update({name: name,phone: phone,});})….}
That's all for our maiden attempt at using Firebase. It works, though there are probably more optimal (and secure) ways to implement it.