Skip to content

About the author of Daydream Drift

Tomasz Niezgoda (LinkedIn/tomaszniezgoda & GitHub/tniezg) is the author of this blog. It contains original content written with care.

Please link back to this website when referencing any of the materials.

Author:

Plight Of Flight

Cover image for "Plight Of Flight".
Published

I’ve been reading through the newest React documentation and ended up on this article about mixins being slowly phased out from the framework. While I agree with it, I also can’t forget how profoundly mixins influenced my experience as a developer. Even though I barely use them anymore, I still think mixins have a lot to offer as an alternative to OOP’s single inheritance and composition which are prevalent nowadays.

In Facebook’s React, mixins are helpers to components and their role limited. For example, React Router’s mixin was used to easily control browsers’ history from inside components. But this functionality can easily be replaced with mechanisms other than mixins.

While mixins are on their way out in React, they won’t be removed soon and are “not technically deprecated”. Too many tools used them until now. But they’re only still available when using the traditional React.createClass function to create React components. ES6 classes, which newer React uses instead, do not support mixins and have divided JavaScript frameworks among those that have compromised and changed their approach to use them, React being one of them, and those that decided to stick to custom ways of defining components, such as Ember and its Ember.Component.extend function.

For practical purposes and based on Facebook’s vast knowledge of how programmers have used React at Facebook, higher-order components may be the most sensible replacement for mixins in React. One of their advantages compared to other frameworks, is they don’t have to modify a component’s HTML body and can still extend it. Many JavaScript frameworks, for example Ember — do, either because a single, containing HTML tag is required for each component or because each component is wrapped in an artificial HTML tag during runtime for the framework to control the change of HTML on the website based on its state.

I’ve had experimented with mixins for years before finally understanding how they work and how they should be implemented to be comfortable to use. The first and only complete implementation I’ve ever come across exists within Twitter Flight. It saddened me to hear this framework is no longer under development but I understand why.

Mixins were never easy to master and reason about and yet they were utilized heavily in Flight, more so than in React. However, once implemented, Flight mixins worked great. I always found it pleasurable having to revisit source code written in Flight, because it was simple and easy to understand.

Why Flight Stood Out Among Others

The idea behind Flight is straightforward and involves an old design pattern. There is no way for components to communicate directly with each other. Like the Yahoo website in the 90s, Flight uses the website’s DOM and often a common root element in it, as a lightweight mediator between data and view components. The division between these two is a convention. There’s actually only one type of component in Flight.

Data components mostly provide logic for making AJAX requests, storing non view related information and enabled mediating between other Flight components. View components, on the other hand, attach to the DOM’s HTML and change it, they store state related to the visible HTML and handle user interaction.

The fact that Flight components use existing DOM makes the framework ideal for pages rendered on the server with frameworks like Spring. Custom data attributes are used heavily in HTML that Flight uses, so components can receive data from the server to initialize themselves.

React does a similar thing with server-side rendered HTML, except on the client it is attached to a root node. React gets state for the already rendered HTML that came with it from the server and, by default, rechecks every node to make sure it’s in-sync with the available state.

However, Flight’s usefulness diminished once the movement to SPAs gained more momentum. SPAs treat client-side rendering and client-side URL routing with priority and Flight naturally couldn’t cater for them.

Components are rarely nested in Flight. Messages usually reach many components, although only some act upon receiving them. Because this mechanism is so simple, the messages have to contain enough information to be discernible and then processed by only the intended components across the whole website. This, along with the heavy use of mixins, forced websites using Flight to be practically "undividable" — pieces of a website could not be developed in isolation because there was a heightened chance of naming collisions.

This is an example of a data component in Flight:

define(['flight/lib/component', 'jquery', 'logger'], function (component, $, logger) {
  function NotificationData() {
    this._request = null

    this._done = false

    this._doneContent = null

    this.defaultAttrs({
      notificationsDownloadUrl: null
    })

    this._onNotificationsDone = function (content) {
      this._request = null
      this._done = true
      this._doneContent = content
      logger.info('notifications loaded: ')
      logger.info(content)
      this.trigger(document, 'notificationsProvided', {
        content: content
      })
    }

    this._onNotificationsFail = function (errors) {
      this._request = null
      logger.log('could not load notifications')
      logger.log(errors)
      this.trigger(document, 'notificationsProvided', {
        errors: errors
      })
    }

    this._requestNotifications = function () {
      var deferred = $.Deferred()
      if (this._done) {
        deferred.resolve(this._doneContent)
      } else {
        this._request = $.ajax({
          url: this.attr.notificationsDownloadUrl
        }).then(deferred.resolve, deferred.reject)
      }
      return deferred.promise()
    }

    this.onNotificationsRequested = function (event, data) {
      this.trigger(document, 'notificationsRequestStarted')
      if (!this._request) {
        this._requestNotifications().then(
          this._onNotificationsDone.bind(this),
          this._onNotificationsFail.bind(this)
        )
      }
    }

    this.after('initialize', function () {
      this.on(document, 'notificationsRequested', this.onNotificationsRequested)
    })
  }
  return component(NotificationData)
})

This, in turn, is a base view component that can be extended with mixins:

define(['jquery'], function ($) {
  function Lightbox() {
    var _isVisible = false,
      oldOverflow = null

    this.defaultAttrs({
      lightboxShadowSelector: '.lightbox',
      closeOnBackgroundClick: true,
      disableScroll: true,
      startVisible: false
    })

    this.onInstanceClick = function (event, data) {
      if (
        this.attr.closeOnBackgroundClick &&
        $(event.target).is(this.attr.lightboxShadowSelector)
      ) {
        this.hide()
      }
    }

    this.isVisible = function () {
      return _isVisible
    }

    this.show = function () {
      if (!this.isVisible()) {
        _isVisible = true
        if (this.attr.disableScroll) {
          oldOverflow = $(document.body).css('overflow')
          $(document.body).css('overflow', 'hidden')
        }
        $(document.body).append(this.$node)
      }
    }

    this.hide = function () {
      if (this.isVisible()) {
        _isVisible = false
        $(document.body).css('overflow', oldOverflow)
        this.$node.detach()
      }
    }

    this.after('initialize', function () {
      this.on('click', this.onInstanceClick)
      if (this.attr.startVisible) {
        this.show()
      }
    })
  }

  return Lightbox
})

Here’s a component extending a base component with mixins:

define([
  'flight/lib/component',
  'mixins/Lightbox',
  'mixins/FoggyLightbox',
  './CancelableLightbox'
], function (component, Lightbox, FoggyLightbox, CancelableLightbox) {
  function CompleteLightbox() {
    this.onUserComplete = function (event, data) {
      this.show()
    }

    this.after('initialize', function () {
      this.on(document, 'userComplete', this.onUserComplete)
    })
  }

  return component(CompleteLightbox, Lightbox, FoggyLightbox, CancelableLightbox)
})

And this a mixin:

define(['logger'], function (logger) {
  function CancelableLightbox() {
    this.defaultAttrs({
      cancelButtonSelector: '.button-lightbox-close',
      keyboardCancel: true,
      keyboardKey: 27
    })

    this.onCancelButtonClick = function (event, data) {
      this.hide()
    }

    this.onCancelKeydown = function (event, data) {
      if (event.which == this.attr.keyboardKey) {
        this.hide()
      }
    }

    this.after('show', function () {
      if (this.attr.keyboardCancel) {
        logger.log('attaching keydown')
        $(document.body).on('keydown', this.onCancelKeydown.bind(this))
      }
    })

    this.before('hide', function () {
      if (this.attr.keyboardCancel) {
        logger.log('detaching keydown')
        $(document.body).off('keydown', this.onCancelKeydown.bind(this))
      }
    })

    this.after('initialize', function () {
      this.on('click', {
        cancelButtonSelector: this.onCancelButtonClick
      })
    })
  }

  return CancelableLightbox
})

Another mixin:

define(['jquery', 'css-filters'], function ($, Modernizr) {
  function FoggyLightbox() {
    this.before('show', function () {
      if (Modernizr.cssfilters) {
        $('.page').css('-webkit-filter', 'blur(5px)')
        $('.page').css('filter', 'blur(5px)')
      }
    })

    this.after('hide', function () {
      if (Modernizr.cssfilters) {
        $('.page').css('-webkit-filter', 'none')
        $('.page').css('filter', 'none')
      }
    })
  }
  return FoggyLightbox
})

To make an instance of a component visible on the website, it has to attach to some existing HTML, for example:

<div style="display: none">
  <div class="lightbox lightbox-generic-complete">
    <div class="lightbox-container lightbox-container-complete">
      <div class="lightbox-container-header">
        <div class="icon icon-close-2 button-lightbox-close"></div>
        <h2><spring:message code="change.profile.success.title" /></h2>
      </div>
      <span class="lightbox-content-complete">
        <spring:message code="change.profile.success.description" />
      </span>
    </div>
  </div>
</div>

How view and data components are attached to a subpage:

define(['jquery', './context/Homepage', 'components/LoginForm', 'data/LoginData'], function (
  $,
  Homepage,
  LoginForm,
  LoginData
) {
  function init() {
    var homepageSelector = '.homepage'

    if (!$(homepageSelector).length) {
      return
    }

    Homepage.attachTo(document)

    LoginData.attachTo(document, {
      checkUrl: window.configuration.ajax.checkLogin
    })

    LoginForm.attachTo('.form-login', {
      focusItemSelector: '.form-login-email'
    })
  }
  return init
})

By the way, the above source code uses AMD which is not recommended for today’s websites.

But even with a higher chance of collisions, websites are “cleanly” implemented: it is easy to place existing components on a new subpage and combine mixins into a component with the needed set of functionalities.

For example, a base lightbox component could be extended with a close button, a blurred background and the ability to get its content using an AJAX call.

A mix of functionalities often requires the use of a modified version of base HTML used by the base component. For the mentioned lightbox, it would add a background element, a close button and a placeholder for downloaded content. HTML templates for more complicated components have to combine all the things from the base component and mixin to support extra behavior.

Flight has multiple mechanisms to make using mixins easier. First, components have a few lifecycle methods, similar to React’s, such as initialize and teardown. These lifecycle methods can be used in mixins and extend the base component’s logic. Second, there is advice. With it, mixins can attach extra logic before, after or around existing functions. Third, default properties from the base component and mixins are all combined automatically. With all of that, using mixins is streamlined in Flight.

For the most part, base components tend to be very dumb. With the mixin pattern, deciding how much logic is shared using a base component and how much goes inside its mixins is difficult. In practice, very little goes inside the base component, because most is needed only by some specific functionality that the component can acquire by using a mixin. Mixins are also overwhelmingly incompatible with different base components. For example, using mixins intended for a lightbox component to extend a form component doesn’t make sense, unless they’re used for logging, routing or similar, generic actions.

It’s Over, We Moved On

All in all, using Flight on a website required careful planning. The mixin and messaging models aren’t very popular. While they are implemented well in Flight, organizing a website with their use isn’t simple. Still, I think Flight was an important step in the “framework wars”. It provided an interesting alternative against frameworks and libraries like KnockoutJS, Ember and AngularJS.

Twitter Flight made using mixins great, maybe even as good as it could ever be. I think it’s justified that the ideas it introduced are be remembered.

Traits in JavaScript are an interesting offshoot of mixins. Sadly, they don’t solve most of mixins’ problems as much as Flight does and mainly add more security and tools for handling conflicts between mixins when they are used in augmenting the same object. Traits are used sometimes today, for example they’re supported natively in PHP but not very popular in practice.

I have no doubt that newer frameworks are better for web development today than Flight was. But, frameworks that gain popularity often comprise of just good implementations of ideas from many years before. I wouldn’t be surprised if mixins found their way back into source code again, maybe for tackling a specific use case that’s not known yet.