In our last blog post, we discussed best practices for using arrays in Firebase. If you haven't read that, please start there. This blog post will discuss advanced concepts for converting between real-time objects and sortable, filterable arrays.

Okay, so let's get some working gloves on and start handling synchronized arrays. If we want client-side sorting, filtering, and other things arrays are great at, but also need the awesome collaborative aspects of real-time objects, then we're going to be up to our elbows in code. We're going to work in JavaScript here, because that's what I'm most familiar with. All of these techniques can be translated to iOS and Android as well.

Using Existing Libraries

Before we get started, it's worth mentioning that there are some great frameworks that have already resolved the problems we face with synchronized arrays. In fact, we'll be drawing from the principles of their array management throughout this article.

EmberFire provides array handling out of the box, integrating Firebase neatly into the Ember.js framework. If you're already a glowing Ember dev, check out the Ember + Firebase Guide for details.

AngularFire version 0.8.0 provides utilities for using arrays in Angular. If you are an ngDev, Check out the Angular + Firebase Guide for details.

BackboneFire provides similar capabilities for those osteologists who prefer Backbone's flexible MVVM.

Best Practices for Synchronized Arrays

Drawing from the lessons learned in developing libraries like AngularFire, we can lay down a solid set of principles to keep ourselves out of trouble:

  • Make the synchronized array read only (use setter methods instead of splice/pop/etc)
  • Add a unique id (the Firebase key) to each record
  • Server changes get written directly into our array
  • Local changes are pushed to the server, then trickle back

In other words, our array is essentially a one-directional loop. Changes come from the server into the array, we read them out, we push our local edits to the server, they trickle back into the array.

Okay, let's get started handling synchronized arrays in Firebase.

Pulling Data from Firebase

Let's start simple:

function getSynchronizedArray(firebaseRef) {
  var list = [];
  // put some magic here
  return list;
}

This provides a method we can pass a firebaseRef into and get back an array. Now let's put some data into it.

function getSynchronizedArray(firebaseRef) {
  var list = [];
  syncChanges(list, firebaseRef);
  return list;
}

function syncChanges(list, ref) {
  ref.on('child_added', function _add(snap, prevChild) {
    var data = snap.val();
    data.$id = snap.key(); // assumes data is always an object
    var pos = positionAfter(list, prevChild);
    list.splice(pos, 0, data);
  });
}

// similar to indexOf, but uses id to find element
function positionFor(list, key) {
  for(var i = 0, len = list.length; i < len; i++) {
    if( list[i].$id === key ) {
      return i;
    }
  }
  return -1;
}

// using the Firebase API's prevChild behavior, we
// place each element in the list after it's prev
// sibling or, if prevChild is null, at the beginning
function positionAfter(list, prevChild) {
  if( prevChild === null ) {
    return 0;
  }
  else {
    var i = positionFor(list, prevChild);
    if( i === -1 ) {
      return list.length;
    }
    else {
      return i+1;
    }
  }
}

Okay, now our array is getting populated from Firebase. Next, let's deal with the other CRUD operations, and move events.

function syncChanges(list, ref) {
  ref.on('child_added', ...); // example above

  ref.on('child_removed', function _remove(snap) {
    var i = positionFor(list, snap.key());
    if( i > -1 ) {
      list.splice(i, 1);
    }
  });

  ref.on('child_changed', function _change(snap) {
    var i = positionFor(list, snap.key());
    if( i > -1 ) {
      list[i] = snap.val();
      list[i].$id = snap.key(); // assumes data is always an object
    }
  });

  ref.on('child_moved', function _move(snap, prevChild) {
    var curPos = positionFor(list, snap.key());
    if( curPos > -1 ) {
      var data = list.splice(curPos, 1)[0];
      var newPos = positionAfter(list, prevChild);
      list.splice(newPos, 0, data);
    }
  });
}

Great, now our array is completely synchronized with the remote data. Now we just need some local editing capabilities.

Pushing Local Updates

We won't modify our array directly. We can do ops like sort to re-order the data, but if we start trying to
do ops like splice or pop, and have the server modifying this array as well, things will get ugly.

Instead, we'll directly push changes to the server with some quick wrapper methods. Since Firebase triggers events
for local changes immediately, without waiting for a server response, this is quite performant and simplifies the
process as well:

function getSynchronizedArray(firebaseRef) {
  var list = [];
  syncChanges(list, firebaseRef);
  wrapLocalCrudOps(list, firebaseRef);
  return list;
}

function syncChanges() { /* in examples above */ }

function wrapLocalCrudOps(list, firebaseRef) {
   // we can hack directly on the array to provide some convenience methods
   list.$add = function(data) {
      return firebaseRef.push(data);
   };

   list.$remove = function(key) {
     firebaseRef.child(key).remove();
   };

   list.$set = function(key, newData) {
     // make sure we don't accidentally push our $id prop
     if( newData.hasOwnProperty('$id') ) { delete newData.$id; }
     firebaseRef.child(key).set(newData);
   };

   list.$indexOf = function(key) {
     return positionFor(list, key); // positionFor in examples above
   }
}

And there we are! We can now manipulate our array by using operations like this:

var ref = new Firebase(URL);
var list = getSynchronizedArray(ref);

// add a record
var newRecordId = list.$add({ foo: 'bar' }).key();

// remove a record
list.$remove(recordKey);

// update a record
var data = list[5];
data.foo = 'baz';
list.$set( data.$id, data );

A Working Example

That's pretty much it! There are a few additional edge cases that we can cover, but we've created a
synchronized array that merges local changes with server updates and keeps all of it in a tidy array.

Here's an example project utilizing these principles you can use
as a template for baking your own solutions. Check out the README for installation instructions.

More To Come!

At Firebase, we're never satisfied with just being awesome. We are constantly working to improve all these scenarios and have several API enhancements in the pipeline. Keep an eye on the Google Group for details and announcements.

Find this article useful or have additional questions? Leave a comment below or post a message on the Google Group!