The most common Angular and Firebase code smell, and how to fix it
You want clean Angular and Firebase code? Here’s my Golden Rule of AngularFire, do not unwrap promises yourself.
The single biggest code smell when building an AngularFire app is using the $loaded() promise.
// WARNING: This is an anti-pattern, don't copy and paste this
// and use it in your own app
function MyController($firebaseArray, itemsRef) {
var syncArray = $firebaseArray(itemsRef);
console.log(syncArray); // empty array at first // it's like peeling an onion
syncArray.$loaded().then(function(items) {
this.items = items; // populated array
}.bind(this));
}
What does it mean to “not unwrap a promise?” It means to avoid calling .then() or .catch(). But, how do you get the data without invoking .then()?
To find out, take a look at using a $firebaseArray().
Why $loaded() smells bad
A $firebaseArray() starts out as an empty array. When the initial data set downloads from the realtime database, it populates that empty array. The $loaded() method can be used to detect when the data has finished downloading.
var syncArray = $firebaseArray(itemsRef);
console.log(syncArray); // empty array syncArray.$loaded().then(function(items) {
console.log(items); // populated array
console.log(items === syncArray); // true
});
The .then() function is a code smell, and here’s why:
The desire to use $loaded() is natural, because it sounds like what you want. Give me the data once the it has downloaded from the server. While this seems useful, it’s not.
You should never need $loaded() in a controller.
This is because $firebaseArray() and $firebaseObject() are wicked smart. When the data is downloaded, they trigger Angular’s digest loop which then updates the view.
By using $loaded() you’re writing bulky code that works the same as a one-liner.
// Same result as the anti-pattern in the first code snippet
function MyCtrl($firebaseArray, itemsRef) {
this.items = $firebaseArray(itemsRef);
}
So where should $loaded() be used? It works beautifully with the router.
$loaded() and the Router
Take the following scenario, you want to display a list of todos. But, you also want to prominently display the first todo at the top of the page. This means you’ll have to get the first item from the $firebaseArray(). To get the first item, or any item, the data has to be downloaded.
function MyCtrl($firebaseArray, itemsRef) {
// this works and smells fresh
this.items = $firebaseArray(itemsRef); // we can’t do this even though it would smell great
// this.first = this.items.$keyAt(0);
// so we have to do this, which smells like Limburger cheese
this.items.$loaded().then(function(data) {
this.first = data.$keyAt(0); // data is available
}.bind(this));
}
We’re stuck with $loaded(), again.
So what’s the solution? Make the data available from the get-go by using the router.
$routeProvider.when('/', {
controller: 'MyCtrl as ctrl',
templateUrl: 'view.html',
resolve: {
items: function($firebaseArray, itemsRef) {
// load the view, when the data is available
// this is where $loaded() belongs
return $firebaseArray(itemsRef).$loaded();
}
}
});
The resolve object is your best friend. Every time you resolve a promise in the router, an angel gets its wings.
This right here, is how you abide by the Golden Rule.
Don’t unwrap promises yourself, let Angular do it for you.
Using resolve, Angular will unwrap the promise for you. When Angular invokes the controller, it’ll also inject the resolved data into the constructor.
function MyCtrl(items) { // items is injected by using resolve
// data is 100% available and smells minty fresh
this.items = items;
// the first item is available as well, so fresh and so clean
this.first = this.items.$keyAt(0);
}
The Takeaway
If you find yourself using $loaded(), there’s probably a better way.
It’s worth pointing out that $loaded() works as a great debugging tool. If you need to inspect the downloaded data set or any errors, this is a great place to do so.
Take some time to read the docs on the Firebase website. You’ll likely save more time in debugging down the road.
How do you use $loaded(), and what are some other code smells you’re aware of?