Flight Mixins
We started building Flight.js at Twitter back in 2011. The brainchild of Dan Webb, it’s a component-based framework based on his previous frameworks polished with what we'd learned from working with Twitter’s existing codebase. One feature of the framework we wanted to include from the outset was Angus Croll's mixin model. His blog post on the subject is beautifully readable, and he's also given a presentation on Flight's mixin model at BrazilJS. It's well worth reading both the blog and slide deck now, if you haven't already.
Two and a half years later, with the whole of twitter.com now running on Flight, how do we feel about Flight and mixins? Are they working for us?
Rather than using the classical talking animals to describe our components, I'll try to use real-life twitter components, though I'll be dramatically simplifying for readability.
module.exports = defineComponent(customTimeline,
withBaseTimeline,
withOldItems,
withMinMaxPagination,
withNewItems,
withTimestampUpdating,
withTweetActions,
withTravelingPtw,
withItemActions,
withUserActions,
withGallery,
withViewerFollowState);
function customTimeline() {
this.defaultAttrs({
itemType: 'tweet'
});
}
Here's an example of the code running our new Custom Timelines. As you can see, all the functionality is provided by our mixins. The code for our timeline is powerful, using delegated event handlers to pick up and handle most of the events sourced by the tweets themselves.
Precedence
We've always described mixins as "specialisations". The idea is that you'd write a module, and then extend it with extra functionality.
For example,
module.exports = defineComponent(newTimeline);
We could then add functionality like infinite scroll:
module.exports = defineComponent(newTimeline,
withInfiniteScroll);
But if we break apart this model a little further, then things get complicated. The mixin is able to wrap and override code in our newTimeline model.
function newTimeline() {
this.updateTimeline = function() {
// something
};
}
function withInfiniteScroll() {
this.around('updateTimeline', function(originalFunc) {
// something
originalFunc();
};
}
However, this doesn't really make sense. If we follow this model, then we need to define the core methods of timeline in every single component that wants to use withInfiniteScroll. We also have no way to extend or override the behaviour of the mixin without using another mixin on top.
A far better model would be this:
module.exports = defineComponent(withInfiniteScroll,
newTimeline);
Putting the mixins at the start of the component definition will allow us to override any behaviour of the mixin we want, from our component. This is much more like mixins, traits, inheritance or composition used by other languages (Ruby or Scala used here at Twitter, for example).
If we apply this new model back to our original component code, we get this:
module.exports = defineComponent(withBaseTimeline,
withOldItems,
withMinMaxPagination,
withNewItems,
withTimestampUpdating,
withTweetActions,
withTravelingPtw,
withItemActions,
withUserActions,
withGallery,
withViewerFollowState,
customTimeline);
This is a simple change, but gives far more power to the customTimeline. Flight gives us this flexibility because it treats all the arguments to defineComponent as mixins. If you're using Flight.js in the wild, I would strongly recommend making this change.
Inheritance
One thing that's very clear from our Custom Timeline example is that our mixins have become reasonably verbose. If we wanted to create another similar Timeline component, we'd have to duplicate that whole definition.
module.exports = defineComponent(withBaseTimeline,
withOldItems,
withMinMaxPagination,
withNewItems,
withTimestampUpdating,
withTweetActions,
withTravelingPtw,
withItemActions,
withUserActions,
withGallery,
withViewerFollowState,
newTimeline);
function newTimeline() {
this.defaultAttrs({
itemType: 'tweet'
});
}
Something we've done in a few cases is create mixin collections:
var compose = require('flight/lib/compose');
module.exports = customTimelineMixins;
function customTimelineMixins() {
compose.mixin(this, [
withBaseTimeline,
withOldItems,
withMinMaxPagination,
withNewItems,
withTimestampUpdating,
withTweetActions,
withTravelingPtw,
withItemActions,
withUserActions,
withGallery,
withViewerFollowState
]);
}
This will work, but I dislike having to reach into the framework and apply the mixins myself. This is also taking us back to the idea of inheritance:
module.exports = defineComponent(customTimelineMixins,
customTimeline);
module.exports = defineComponent(customTimelineMixins,
newTimeline);
Ultimately, I think there is some value in this combination of mixins and inheritance. In wider discussions with the team at Twitter, we took this further. Ideally, we'd like to be able to treat a defined component just as another mixin.
module.exports = defineComponent(customTimelineComponent,
newTimeline);
This isn't possible in the framework today, but conceptually there's no reason why we couldn't treat every component as a collection of mixins, and defer the creation of the component itself until it is actually used for instantiation and attachment. I'm hoping something like this will make Flight v2.
Modularity
One of the huge benefits of Flight is the enforcement of modularity. Each component is written and tested as a unit, and the only communication layer is through events.
However, large components are more complex to test, and when there are lots of mixins, it can be really hard to trace through the code, especially when advice is used liberally.
If you've got a lot of mixins, as we clearly do for our timelines, then you might be better breaking the component apart, into multiple components.
Our current timeline component is attached like this:
CustomTimeline('#timeline', options);
But there's no reason why we couldn't have done something like this:
CustomTimeline.attachTo('#timeline', options);
TimelinePagination.attachTo('#timeline', options);
TimestampUpdating.attachTo('#timeline', options);
TimelineActions.attachTo('#timeline', options);
Of course, we could attach those components within CustomTimeline:
function customTimelineMixins() {
this.before('teardown', function() {
this.trigger('teardownTimelineSubcomponents');
});
this.after('initialise', function() {
TimelinePagination.attachTo(this.$node, options);
TimestampUpdating.attachTo(this.$node, options);
TimelineActions.attachTo(this.$node, options);
});
}
However, as you can see, we have to be clever about handling our own teardowns here, which may not be worth the hassle.
Attributes
If you've used mixins at all, you've likely encountered the default attribute conflict. If an attribute in declared in a mixin, it cannot be redeclared in your component, or even in another mixin. Our aim was to prevent unintentional 'clobbering' of methods and properties, where two mixins use the same property for different purposes, producing unexpected and hard-to-debug conflicts.
However, I've often wanted to avoid this guard, to give more specific default attributes for my mixins in my component definition. To do so, we can define a new method, "overrideDefaultAttrs":
this.overrideDefaultAttrs = function(defaults) {
utils.push(this.defaults, defaults, false) || (this.defaults = defaults);
};
This will behave exactly the same as the defaultAttrs method, but will pass "false" to utils.push, which will allow us to overwrite the attributes when conflicts arise.
It's tempting to redefine defaultAttrs itself, but I think it's better not to redefine the framework for future compatibility, and the guard may prove useful in other places. The method is also blocked from redefinition when being mixed in.
To add the method above, one could either add it to the global component base (be sure to do so before any components are defined):
define(['./base', './utils'], function(base, utils) {
this.overrideDefaultAttrs = function(defaults) {
utils.push(this.defaults, defaults, false) || (this.defaults = defaults);
};
});
Or simply create a mixin to be applied to each component as you need it.
We haven’t yet tried this in-house, but I think it would solve some readability and precedence issues.
Summary
Mixins have proved to be a vital part of Flight and twitter.com. Key learnings:
- move your mixins to the start of the component definition
- consider using overrideDefaultAttrs in component definitions
- only use mixins where appropriate; use different components if you can
- keep an eye on Flight for new features
Thanks for reading! I guess you could now share this post on TikTok or something. That'd be cool.
Or if you had any comments, you could find me on Threads.
Published