Observable Data Structures in JavaScript

Wouldn’t it be great if native JavaScript data structures allowed for events to be emitted? That was my goal about a year ago while refactoring a SharePoint set of widgets that display data consumed from Lists and Libraries. That lead me to create a library I called ObservableData, that in addition to enabling me to “watch” native Array and Objects for changes, also provides a way to create “computed” object properties – properties whose value is composed from a set of dependencies (values from other objects) and, most importantly, are automatically updated when those dependencies change. I have finally found the time to post that library to GitHub, here, and write this post.

Observables

In any JavaScript application, you primarily use Arrays and Objects to store and pass around data between different areas of the app. One thing that these two data structures don’t provide is for a way to listen or watch them for changes, which can then lead to other logic being triggered and likely the UI updated, thus providing feedback to the user.

I first worked with observable data with KnockoutJS and love the pattern.  Your data is defined upfront and as it changes, events are emitted and listeners react. Unlike KnockoutJS’s approach, I did not want to have a custom structure to represent my data values, but rather want to add the missing functionality to the Array or and Object instance structures so that it stays close to native structures already provided by the JavaScript language.  So an array should still behave like an array and have available all of its existing methods (push, shift, forEach, reduce, map, etc.), and Objects should continue to support accessing and setting its keys directly (ex.: user.firstName) and work with the API provided by the JavaScript Object primitive.

There have been attempts to add this functionality to JavaScript in the past. Object.observe and Array.observe were at one point being considered and even made to some versions of Chrome. ES2015 introduced Proxy, but unfortunately it can’t be Polyfilled (to my knowledge) for older browsers. Then, I started to use VueJS and was exposed to their Reactivity… What a pleasant experience. I knew then that I needed to create a standalone library to achieve a similar behavior. A quick look at Object.defineProperty revealed that doing so should not be that difficult, so off I went.

Lets look at what I’m looking for in an Object:


const user = ObservableObject.create({firstName: "paul"});

user.on("firstName", () => console.log(user.firstName));

user.firstName = "PAUL"; // console logs "PAUL"

So I create an object, then I add a listener to the firstName property so that when it changes, it writes to the console the new value. The user variable is a plain JavaScript Object.  You can use Object.keys() on it or any of the other operations available with Objects.

For Arrays, I would like a very similar approach:


const allUsers = ObservableArray.create([]);

allUsers.on("change", () => console.log(allUsers.length));

allUsers.push(user); // console logs "1"

Here again I create an Array, set up a listener for change event that when triggered logs the size of the array to the console.

Simple right? I think so 🙂

Computed Properties

Computed properties are just as easy to create.  Here is an example:


const user = ObservableObject.create({firstName: "Paul", lastName: "Tavares"});

ObservableObject.createComputed(user, "fullName", () => `${user.firstName} ${user.lastName}`);

user.fullname; // "Paul Tavares"

user.firstName = "PAUL";

user.fullName; // "PAUL Tavares"

Computed properties are very efficient in that their value is not actually calculated until it is accessed the first time.  Only then will the function provided during setup (createComputed) is called and value cached until one of its dependencies change.  So if you create 10 computed properties on an Object, but never really access their value, then the value generator function will never actually run.

Computed property values are not limited to the object where the computed is being created. They will track dependencies across any Observable object, thus allowing for much broader usage.  There is another example:


const user = ObservableObject.create({firstName: "Paul", lastName: "Tavares"});

const office = ObservableObject.create({city: "New York", location: "Floor 1, C134"});

ObservableObject.createComputed(user, "fullName", () => `${user.firstName} ${user.lastName}`);

ObservableObject.createComputed(user, "about", () => `${user.fullName} from ${office.city} (${office.office})`);

In the above example you can see the computed property about that was added to the user object uses information from both objects (user and office. Updating any of the values used in that computed property will trigger it to be updated.

Of the two data structures in this library, ObservableArray is the one that needs more work outside of the above basic support. I will be improving this one so that it can also be used with computed properties (ex. trigger computed property updates when the array changes) as well as to expose a functions so that it may be used in FP.

Functional Programming (FP) Approach

In addition to providing Constructors (ObservableObject and ObservableArray), the library also exports functions that allow for its features to be applied to existing objects without polluting them with additional API methods.  Such as:


import { watchProp } from "observable-data"

const user = { firstName: "paul" };

const firstNameEvListener = watchProp(user, "firstName", () => console.log("firstName changed"));

The above uses the watchProp function and places an event listener on the firstName property of the  user object and writes to the console every time it changes.  Several other functions are available, like observableAssign, makeObservable, watchPropOnce, notifyPropWatchers. The object used in the example above (user) remains intact by having only 1 property (firstName) and with none of the methods that would have been provided with the Factory Constructor.


So that is how I now manage/work with data in JavaScript in my side projects. I’m a big fan of Observables and really like the ability to drive user interfaces from changes to internal data. This library enables me to do that without deviating to far away from the native data types already provided by JavaScript.

In a future post I will highlight how I use this with a my own DOM binding utility to keep the User Interface updated (and will also clean up and publish that DOM binding utility).

Advertisements

One thought on “Observable Data Structures in JavaScript

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s