At Urban Airship, we’ve developed a novel approach to building our web application. The basic underlying components are described here, but as your web application grows in complexity, the problem of determining the current state of the page based on the behavior of each component becomes non-trivial. ObjectState is our solution to this problem.
Front end development is a lot like plumbing.
The client wants to send a push, but doesn’t know how to use an API directly, so we set up a dynamic html5 form that gives them live feedback as they enter the specification of the push. Have they gone over the previously restrictive iOS byte limit? Did they select an option that requires additional input? Did they typo an iOS device identifier? These questions have a common characteristic: Their resolution involves updating state over time.
Our user interface is expressed in terms of through streams, a user-land utility for quickly creating node streams. Streams are an abstraction for modelling data over time– the metaphor is that data flows through the stream, and can be redirected via piping from one stream to another. UI components, meaning a visual element on the screen with which the user can interact, and the units of logic that perform validation, preview, or submission, are all implemented in streams. Data is piped from network-request-making streams to transform and error handler streams, and then into a stream which updates the user interface. User manipulation of the interface is represented as a stream which we pipe to validation streams, and then, should the validation succeed, a stream which issues network requests, beginning the cycle again. We manage the flow of data from client to server and back, and that’s the job.
This can get complicated when the interface grows complex. How do you model the state of a complicated form, like the one sending a push described above? Each input element and selector has a corresponding stream, but, in order to evaluate whether the push that would result from the user input is valid, we need to aggregate all the flows of data into a single structure representing the state of the page. Then we can validate and ultimately translate this structure into a request against our push API.
ObjectState: What does it do?
We accomplish this aggregation and updating via a module called ObjectState, available on npm. This module’s explicit purpose is to be the single source of truth for the combined last-seen state of an arbitrary number of streams (really event emitters, of which streams are a sub-class).
The main benefit of this arrangement is that it permits us to easily encapsulate and compose asynchronous processes. Sources of data to which the ObjectState instance subscribes need only have knowledge of their own concerns. They don’t need to know about the greater significance of the data they represent, they merely surface it for consumption downstream.
How does this play into our push-sending form? Each option, toggle, or button is modelled as a stream. An ObjectState (or a heirarchy of them, it doesn’t matter), listens to streams representing user input and updates itself appropriately. A single module listens to the state, and applies a series of validator functions, providing the user with near-instantaneous feedback about the validity of their proposed push. Another module translates the state into a JSON API payload, which can be saved or sent. Each module exports a stream, each propagates updates when the user makes a change. If external input can impact the composition of a push (e.g. the ticking of the clock when scheduling a push), then that too is represented as a stream to which the ObjectState listens.