Integrating Firebase Realtime Database

It's time to integrate Firebase in our application. Though we have already seen the detailed description and features of Firebase Realtime Database in Chapter 2Connecting React to Redux and Firebasewe will see key concepts for JSON data architecture and best practices for the same. Firebase database stores data as a JSON tree.

Take into consideration this example:

{
"seats" : {
"seat-1" : {
"number" : 1,
"price" : 400,
"rowNo" : 1,
"status" : "booked"
},
"seat-2" : {
"number" : 2,
"price" : 400,
"rowNo" : 1,
"status" : "booked"
},
"seat-3" : {
"number" : 3,
"price" : 400,
"rowNo" : 1,
"status" : "booked"
},
"seat-4" : {
"number" : 4,
"price" : 400,
"rowNo" : 1,
"status" : "available"
},
...
}
}

The database uses a JSON tree, but data stored in the database can be represented as certain native types to help you write more maintainable code. As shown in the preceding example, we have created a tree structure like seats > seat-#. We are defining our own keys, such as seat-1, seat-2, and more, but if you use the push method, it will be autogenerated.

It is worth noting that Firebase Realtime Database data nesting can go up to 32 levels deep. However, it is recommended that you avoid the nesting as much as possible and have the flat data structure. If you have a flattened data structure, it provides you with two main benefits:

  • Load/Fetch data that is needed: You will fetch only the required data and not a complete tree, because in the case of a nested tree if you load a node, you will load all the children of that node too.
  • Security: You give limited access to data because in the case of a nested tree, if you give access to a parent node, it essentially means that you also grant access to the data under that node.

The best practices here are as listed:

  • Avoid Nested Data
  • Use Flatten data structures
  • Create Scalable Data

Let's first create our Realtime Firebase database:

We can directly create this structure on Firebase console or create this JSON and import it in Firebase. We have the following structure to our data:

  • Seats: Seats is our main node and contains a list of Seats
  • Seat: Seat is an individual object that represents a seat with a unique number, price, and status

We can design a three-level deep nested data structure, such as seats > row > seat, for our sample application, but as mentioned in the best practices earlier, we should design a flattened data structure.

Now that we have our data designed, let's integrate the Firebase in our application. In this application, instead of adding Fireabase dependency through URL, we will add its module using npm:

npm install firebase

This command will install Firebase module in our application, and we can import it using the following statement:

import firebase from 'firebase';

The import statement is ES6 feature, so if you are not aware of it, refer to ES6 documentation at http://es6-features.org/.

We will put our DB related files in a folder called API.

api/firebase.js:

import firebase from 'firebase'
var config = { /* COPY THE ACTUAL CONFIG FROM FIREBASE CONSOLE */
apiKey:"AIzaSyBkdkAcHdNpOEP_W9NnOxpQy4m1deMbG5Vooo",
authDomain:"seat-booking.firebaseapp.com",
databaseURL:"https://seat-booking.firebaseio.com",
projectId:"seat-booking",
storageBucket:"seat-booking.appspot.com",
messagingSenderId:"248063178000"
};
var fire = firebase.initializeApp(config);
export default fire;

The preceding code will initialize the Firebase instance that can be used to connect to Firebase. For better separation of concern, we will also create a file called service.js, which will interact with our database.

api/service.js:


import fire from './firebase.js';

export function getSeats() {
let seatArr = [];
let rowArray = [];
const noOfSeatsInARow = 5;

return new Promise((resolve, reject) => {
//iterate through seat array and create row wise groups/array
const seatsRef = fire.database().ref('seats/').orderByChild("number");
seatsRef.once('value', function (snapshot) {
snapshot.forEach(function (childSnapshot) {
var childData = childSnapshot.val();
seatArr.push({
number: childData.number,
price: childData.price,
status: childData.status,
rowNo: childData.rowNo
});
});

var groups = [], i;
for (i = 0; i < seatArr.length; i += noOfSeatsInARow) {
groups = seatArr.slice(i, i + noOfSeatsInARow);
console.log(groups);
rowArray.push({
id: i,
seats: groups
})
}
console.log(rowArray);
resolve(rowArray);
}).catch(error => { reject(error) });
})

}

export function bookSelSeats(seats) {
console.log("book seats", seats);
return new Promise((resolve, reject) => {
//write logic for payment
seats.forEach(obj => {
fire.database().ref('seats/').child("seat-" + obj.number)
.update({ status: "booked" }).then(resolve(true)).catch(error => { reject(error) });
})
});

}

In this file, we have mainly defined two functions—getSeats() and bookSelSeats()which are to read database for list of seats and update seats when user checks them out from the cart, respectively.

Firebase provides two methods—on() and once()—to read data at a path and listen for changes. There is a difference between the on and once methods:

  1. The on method: It will listen for the data changes and will receive the data at the specified location in the database at the time of the event. Also, it doesn't return a Promise object.
  2. The once method: It will be called only once and will not listen for changes. It will return a Promise object.

As we are using the once method, we get a Promise object returned to our component object, since the call from our component to the service will be async. You will understand it better in the following App.js file.

To read a static snapshot of the contents at a given path, we can use value event. This method is executed once when the listener is attached and every time the data changes including children. The event callback is passed a snapshot containing all data at that location, including child data. If there is no data, the snapshot returned is null.

It is important to note that the value event will be fired every time the data is changed at the given path, including data changes in children. Hence, it is recommended that we attach the listener only at the lowest level needed to limit the snapshot size.

Here, we are getting the data from Firebase Realtime Database and get all the seats. Once we get the data, we create a JSON object according to the format we need and return it.

App.js will be our container component and will look like the one that follows:

App.js

import React, { Component } from 'react';
import './App.css';
import SeatList from './components/SeatList';
import Cart from './components/Cart';
import { getSeats } from './api/service.js';
import SeatRow from './components/SeatRow';

class App extends Component {
constructor() {
super();
this.state = {
seatrows: [],
}
}

componentDidMount() {
let _this = this;
getSeats().then(function (list) {
console.log(list);
_this.setState({
seatrows: list,
});
});

}

render() {
return (
<div className="layout">
<SeatList title="Seats">
{this.state.seatrows.map((row, index) =>
<SeatRow
seats={row.seats}
key={index}
/>
)}

</SeatList>
<hr />
<Cart />
</div>
)


}
}

export default App;

Here, we can see that the App component maintains the state. However, our goal is to separate the state management from our presentational component and use Redux for it.

So, now we have all functional pieces ready, but how will it look without proper design and CSS. We have to design a seat layout that is user-friendly, so let's apply CSS. We have a file called App.css for the entire app. We can separate them out in different files if required.

App.css:

.layout {
margin: 19px auto;
max-width: 350px;
}
*, *:before, *:after {
box-sizing: border-box;
}
.list {
border-right: 4px solid grey;
border-left: 4px solid grey;
}

html {
font-size: 15px;
}

ol {
list-style: none;
padding: 0;
margin: 0;
}

.seatrow {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
}
.seatobj {
display: flex;
flex: 0 0 17.28571%;
padding: 5px;
position: relative;
}

.seatobj label {
display: block;
position: relative;
width: 100%;
text-align: center;
font-size: 13px;
font-weight: bold;
line-height: 1.4rem;
padding: 4px 0;
background:#bada60;
border-radius: 4px;
animation-duration: 350ms;
animation-fill-mode: both;
}

.seatobj:nth-child(2) {
margin-right: 14.28571%;
}
.seatobj input[type=checkbox] {
position: absolute;
opacity: 0;
}
.seatobj input[type=checkbox]:checked + label {
background: #f42530;
}

.seatobj input[type=checkbox]:disabled + label:after {
content: "X";
text-indent: 0;
position: absolute;
top: 4px;
left: 49%;
transform: translate(-49%, 0%);
}
.seatobj input[type=checkbox]:disabled + label:hover {
box-shadow: none;
cursor: not-allowed;
}

.seatobj label:before {
content: "";
position: absolute;
width: 74%;
height: 74%;
top: 1px;
left: 49%;
transform: translate(-49%, 0%);
border-radius: 2px;
}
.seatobj label:hover {
cursor: pointer;
box-shadow: 0 0 0px 3px yellowgreen;
}
.seatobj input[type=checkbox]:disabled + label {
background: #dde;
text-indent: -9999px;
overflow: hidden;
}

We are done with our minimal seat booking app. Yay! The following are the screenshots of the application.

The next screenshot shows the default layout where all the seats are available for booking:

The following screenshot shows that the Booked tickets are marked as X, so the user can't select them. It also shows that when a user selects a seat, it turns out to be red so that they can know which seats have been selected by them:

Finally, we have our seat booking app ready where we are loading data from Firebase database and showing them using React. However, after looking at the preceding screenshot, you must be thinking that though we have selected two seats, the cart is empty and is not showing any data of the seats. If you remember, we haven't written any logic in the seat click handler function to add the selected seats in cart and hence our cart remains empty.

So, now the question would be, since Seat and Cart components are not directly related to each other, how will a Seat component communicate with the Cart component? Let's find an answer to this question.

When components are not related or are related but too far away in the hierarchy, we can use an external event system to notify anyone who wants to listen.

Redux is a popular choice to handle the data and events in a React application. It's a simplified version of the Flux pattern. Let's explore Redux in detail.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset