Using JavaScript Promises to Reason About User Interaction

Sage Griffin

In functional programming, we rarely worry about concurrency primitives like threads. Instead, we use abstractions to make our code easier to reason about. In JavaScript, a Promise is the most common abstraction we encounter. It is usually associated with an Ajax request. We often use abstractions to reason about our concurrent code, but those abstractions can apply in other situations, as well. High level user interactions are essentially large asynchronous events, and can be represented as such in code.

Example: A video storefront

Let’s take a look at an example. We’re working on a video store front. The user can buy videos, and play videos that they own. If they aren’t signed in, they need to sign in to buy a video, and we’d like to use modal dialogs for the sign in and purchasing process.

In order to determine if a user already owns the content, we need them to be signed in. It’s also possible that upon signing in, they could already own the content they tried to purchase. This can quickly lead to an extremely complex set of conditionals, but we can simplify it by thinking about the problem like we would think about concurrency.

Making promises about user interactions

Any time we’re dealing with user interaction, it is inherently asynchronous. In most code, this begins and ends with event handlers on individual DOM elements. This can be better abstracted if we think about the larger interactions a user can take, and represent that as a single asynchronous operation in the code.

A user may or may not be signed in. We could expose that, and let that conditional bubble throughout our code. However, most of the time, if a user isn’t signed in, we’re just going to prompt them to sign in. That means that rather than having a user who may or may not be signed in, we’ve always got a user that will be signed in at some point in the future. Let’s look at how we might represent this in code:

class UserSession
  constructor: (@Q, @AuthModals, @Session) ->
    @_session = @Session.get()

  signIn: ->
    @_authDeferred = @Q.defer()
    @_resolveOrPrompt()
    @_authDeferred.promise

  completeSignIn: (data) ->
    @Session.save(data).then(@_completeAuth)

  _resolveOrPrompt: ->
    if @_session?.signedIn
      @_completeAuth(@_session)
    else
      @AuthModals.openSignIn()

  _completeAuth: (session) ->
    @_session = session
    @_authDeferred.resolve(session.user)

There’s a lot going on here. Q is Kris Kowal’s Q library for working with promises. We’ll gloss over the implementation of AuthModals, and Session. We’ll just assume that AuthModals opens the modal dialog, and something will call completeSignIn when the form is submitted. Session does two things. It asks the server if we’re already signed in, and will submit the Ajax request to the server, and return a promise with the session object.

When code calls the signIn method, we’re returning a promise that will eventually be resolved with the current user. If they’re already signed in, we resolve that promise immediately. Otherwise, we open the modal dialog for the user to sign in, and resolve the promise once the form is submitted.

Good patterns are made to be built upon

This is incredibly powerful. We’ve completely removed the need for code to worry about whether or not the user is signed in for almost all cases. We can build upon this further by applying the same concept to purchases.

class UserPurchases
  constructor: (@Q, @UserSession, @PurchaseModals) ->

  purchase: (@video) =>
    @UserSession.signIn().then(@_promisePurchase(video))

  completePurchase: (license)
    @user.addLicense(@video, license)
    @_purchaseDeferred.resolve(license)

  _promisePurchase: (video) => (@user) =>
    @_purchaseDeferred = @Q.defer()
    @_resolveOrPrompt(video)
    @_purchaseDeferred.promise

  _resolveOrPrompt: (video) =>
    if @user.owns(video)
      @completePurchase(@user.licenseFor(video))
    else
      @PurchaseModals.openSaleForm(video)

We apply the same pattern here. If a user already owns the video, we resolve the promise with the license immediately. If not, we prompt the user to buy it, and resolve it when the license is given to us later. A user can only buy one thing at a time. If the user closes the modal and tries to buy something else, the old promise is effectively thrown away.

Finally, we can hide all of this away inside of an object responsible for playing the video.

class VideoPlayer
  constructor: (@UserPurchases, @PlayerModal, @Video) ->

  play: (video) =>
    @UserPurchases.purchase(video).then(@_loadAndPlay(video))

  _loadAndPlay: (video) => (license) =>
    @Video.load(video, license).then(@PlayerModal.openPlayer)

With this in place, code that wants to play a video needs only to call VideoPlayer.play. It doesn’t need to concern itself with whether or not the user is signed in, or whether they already own the content. We would still need to have a conditional inside of the template to decide whether the button should say ‘Buy’ or ‘Play’. However, if we use the Null Object pattern, and give the template a Guest when the user isn’t signed in, this still remains relatively simple.

By thinking about higher level user interactions the same way we would think about a larger asynchronous computation, we’ve avoided littering our code with ugly conditionals, and are able to isolate concerns like login and purchasing content to a single place. Testing each of these objects individually also becomes much easier than having to deal with the four cases in one place.

These kinds of situations are everywhere if you look for them. You just have to remember that asynchronicity isn’t just for concurrency, and promises aren’t just for XHR.

What’s next

If you found this useful, you might also enjoy: