ES6 Promises with Firebase
Data for the Front-End
I’m a front-end engineer. That’s probably why I like Firebase as a back-end service, with its JSON format, real-time sync, and authentication service. As a developer, I find myself trying to cut the server out of the equation and run code almost entirely on the client.
Let’s consider here how to handle data transaction and storage with a remote flat-file database in today’s promising ES6 world!
An App is Born
Let’s pretend we want to architect a sports app that provides two main views:
- a list of teams, with their name and foundation year.
- an individual team from that list, with additional data including a full list of players on the team and its schedule.
Baby Steps
Well, intuitively, thinking about it from a storage perspective, we simply have teams and some related info to consider. It’s well contained. We might think to structure our data as follows, with one single ‘teams’ node at the top:
- teams
- firebaseKey1 // unique id's generated by firebase
- name
- founded
- players (long list)
- schedule (long list)
- firebaseKey2
- repeat...
But this is a no-go. It’s stated in the docs as an anti-pattern. Don’t deeply nest your data. It might work well enough for the individual team view: just fetch the one team and display all of its related info, but it’s disastrous for the list view.
When the user navigates to the list view, we need to fetch the teams data from the top and then filter through it to get the few pieces of data we actually need to display. We’re transmitting and handling all that extra stuff including lists of players and all of the team schedules. It’s a non-starter.
Yet, this was to be expected. After all, we are running a NoSQL cloud database, so we have no relational tools such as:
SELECT Name, Founded FROM Teams;
Let’s Get Serious
It becomes evident that we must structure our data to optimize single read operations, even if that means writing the same information in multiple places to make it more easily accessible. In essence, we want to organize our data like this:
- team-index
- firebaseKey1
- name
- founded
- firebaseKey2
- repeat...
- teams
- firebaseKey1
- name
- founded
- players (long list)
- schedule (long list)
- firebaseKey2
- repeat...
We store each team’s data as well as an index for that team with some duplicated info. Then, we can quickly access either one and avoid ridiculously and unnecessary bulky requests.
As the advice goes, we de-normalize our data structure. It may seem counter-intuitive to create a bigger database to gain on performance, but we have to remember the motto:
Data storage is cheap, the user’s time is not.
By the user’s time, we mean the loading and performance of the app, which is directly affected by the amount of data we transfer and manipulate. If it’s not fast, our app is doomed!
Some Code, Please
For better internal references within our database, let’s use the unique firebase key (generated on ref.push()) in both of our data ‘branches’.
So, adding a new team to our firebase is an async task following this basic procedure:
- Store the new team data
- Get the unique firebase-generated key
- Store the index data
Let’s think in javascript now. Firebase shows this example in the docs for saving data, making use of a completion callback:
dataRef.set(“I’m writing data”, function(error) {
if (error) {
alert(“Data could not be saved.” + error);
} else {
alert(“Data saved successfully.”);
}
});
Promises To The Rescue
We could use that good ole callback approach if we’re feeling nostalgic, but let’s face it, it just get confusing quickly (and it’s just plain ugly). We’ll make use of ES6 Promises to compose our async flow instead. Let’s write two promise factories to suit our purpose.
let addTeam(team) {
return new Promise((resolve, reject) => {
let newRef = dataRef.child('teams').push(team);
if(newRef) {
resolve(newRef.key());
else {
reject("The write operation failed");
}
});
};let addTeamIndex(indexData, key) {
return new Promise((resolve) => {
dataRef.child('team-index').child(key).set(indexData);
resolve();
});
};
Each function returns a promise to write to our Firebase, and resolve or reject depending on the outcome.
You’ll notice I’m not dealing with reject() in the second method. It always resolves. That is because I’m not expecting any data back from this operation (post saving the index), and I’m counting on the Firebase Event Guarantees to sync the data eventually, once the op is fired, even if the network is down.
So now, we are near the end of our little adventure. When adding a new team to the league, we can use the lovely benefits of promises to keep our code clear and concise, the way we love:
addTeam(team).then( key => {
addTeamIndex({
name: team.name,
founded: team.founded
}, key);
}, console.log);
It’s nice and compact, and easy to reason about. The console.log at the end fires only if the addTeam promise is rejected. It’s a nice way to handle errors.
To be sure, this is a contrived example, and the callback syntax works just as well. But inevitably, you will soon need multiple data operations with a denormalized architecture. Promises are now in the language (well, soon enough), and they will help us keep our sanity.
Authentication is another good example, where you might chain a few firebase operations together: check if username is taken THEN create authentication THEN register and login.
How about keeping consistent data?
Indeed, your database could lose consistency. This would happen if we send our team data, and before we get the unique key value necessary for our second operation, the browser crashes or otherwise loses connectivity.
There are multiple ways for dealing with this scenario. One valid option is simply ‘not to care’ but optimize the order of your operations to make occasional inconsistencies irrelevant.
In our example, we write to the teams node before the index node. It’s significant. In this case, if the first data operation never resolves or rejects on the client-side, we are left with an orphaned team which cannot be accessed from the app; which is better than the reverse: having a team in the index (and consequently in the list view) with no corresponding data when the user clicks on that team.
Our data ops are very small and fast. We can hope that losing a promise settlement would happen rarely and have no evil consequences if we structure our data operations carefully. Promises will help plan this ordering as well.
How would you improve on this code and reasoning? Could the Promise.resolve(value) help us be even more concise?
Please comment below.
You can follow me at @collardeau on twitter, and I’m also on github.