the state of front-end at crowdtwist
TRANSCRIPT
...written in various libraries and frameworks...
☞ FanCenter: Backbone.js, Marionette.js, Geppetto, RequireJS
☞ Control Center & Widgets: AngularJS
...using different tools.☞ FanCenter: Crusher (in-house build tool), Mocha
☞ Control Center & Widgets: npm, Bower, Grunt, Express, Karma
Growing painsAs a new member of the team, I would like to learn CoffeeScript, Jade, Sass, as well as Backbone, Marionette, Geppetto, Angular, Grunt, and Crusher, so that I can develop new features.
Some of these may seem relatively simple to pick up and start using, but it becomes hard really fast to write scalable and maintainable code across the different apps and frameworks.
Think bloated views and spaghetti events in Backbone and custom Angular directives with crazy $scope magic and deep equality checks.
❝Just because you're using Backbone and Angular doesn't mean your code has to suck.❞—a reasonable developer
To which I respond...
True! But... it makes it harder to grow an idiomatic codebase. Each framework has its own special sauce and set of rules you need to understand and follow if you want to write maintainable code.
Unless, of course, every developer is a rockstar and their thought process is identical to mine!
That's how it works, right?
Right!?
How did we get here?☞ Magical frameworks
☞ False separation of concerns
☞ Nascent tools and rapidly evolving ecosystem
Frameworks
Frameworks make certain assumptions about how an application will function, providing tools out of the box to greatly simplify the development process as a whole.
Tend to stress thinking less and doing more, faster.
For certain use cases, this is a good solution, eg. basic CRUD interfaces, smaller single-page apps.
Frameworks
When dealing with larger applications and growing pains described earlier, it is valuable to ask the following question:
What are the costs / consequences, if any, of coupling your application to a framework?
Frameworks
Consider a form view leveraging a custom directive to render a dynamic list of inputs:
<div ng-controller="SurveyCtrl"> <form name="surveyForm"> <question-list ng-model="survey.questions" /> </form></div>
Now ask yourself, who owns the view(s)?
Separation of concerns
Separating concerns, such as making requests, validating forms, and rendering dynamic inputs, into separate components makes sense.
Directives make sense, as do route controllers and form validators.
The problem is how these components communicate, the implicit dependencies between them, and being able to answer the original question of who owns the view?
Separation of concerns
If Robert Plant comes along and wants to add a new feature to the existing functionality, he would first need to:
☞ Understand how nested object properties affect a child's $scope
☞ (Kinda) understand the differences between scope, child scope, isolate scope, transculsion
☞ The evils of ngInit and its partner in crime $scope.$watch
☞ $modelValue → $formatters → $viewValue → $render $modelValue ← $parsers ← $viewValue ← $setViewValue
Separation of concerns
This magic makes AngularJS a simplicity trap.
Angular.js is attractive when coming from Backbone.js where you have to do everything yourself (not an ideal situation either).
The problem is you relinquish a non-trivial amount of ownership and control over your application in hopes of the magic paying off and a false sense of simplicity.
When dealing with large applications that consistently grow in features with an increasing amount of developers, ownership and true simplicity become even more important.
No, seriously, what's up with that?☞ The API is radically different
☞ It's essentially a new framework
☞ No support for IE 8 (no one wants to, but we still need to)
☞ Did I mention it's a completely new framework?
We just started building our Angular apps last year!!
Change is good
That's how the radical notion of front-end development came to be, as well as amazing tools like jQuery, Backbone and Angular.
Stability is better
Is the tool simple, small and predictable enough to use now and still support next year?
Turns out that was a hard question to answer last year.
And it's still hard to answer today!
The language is maturing
This means we can simplify our toolchain and remove additional languages like CoffeeScript.
We don't need CoffeeScript, it just makes JavaScript better.
But now, JavaScript is making JavaScript better!
The tooling is maturingimport {get} from "common/request.js";
class MyComponent {...};
export default MyComponent;
❝JavaScript is great and all, but I need to write an app and I'm not gonna start rolling my own router, MVC library and templating solution❞—a reasonable developer
Back to basics
Instead of building controllers to manage views that depend on other controllers that manage views that manage templates that manage DOM, just build self-contained Components.
import React from "react";import Question from "views/question";
let QuestionList = React.createClass({ render() { let questions = this.props.questions.map(question => { return <Question data={question} />; });
return ( <div class="questions"> {questions} </div> ); }});
export default QuestionList;
❝Dude, is that HTML in your business logic? WAT?? You know you're not supposed to...❞—a reasonable developer
Not quite.That HTML is actually JSX, a syntax that is essentially XML and allows for the DOM to be composed alongside your logic.
If you think that's breaking all the rules, newsflash. You're doing it already.
unwatch = $scope.$watch('survey', function(survey) { if (survey) { unwatch(); $scope.survey.hasQuestions = survey.questions.length > 0 ? true : false; }});
<div ng-controller="SurveyCtrl"> <form name="surveyForm"> <input type="text" ng-init="survey.title = survey.title || 'Default'" ng-model="survey.title"> <input type="checkbox" ng-model="survey.hasQuestions"> </form></div>
It may not be as obvious as JSX, but the coupling is still there!
With React, you at least get all the dependencies isolated to a single component in a single file.
That is ownership.
If you still oppose the idea of JSX, you can use React without JSX:
React.createElement(Question, {data: question}, "innerText");
CompositionYour component can render other components!
<QuestionList> <Question /> <Question /></QuestionList>
(without the complexity of isolate scopes and transclusion)
Speed
You may be thinking the (re)rendering of these components is slow, but React is smart.
React operates on the DOM in-memory before making the smallest set of changes to the browser's document.
This is facilitated by a straightforward definition of state.
State
A component's data is managed by 2 objects, props and state.
props is the configuration of your component passed in as attributes. It is immutable.
state is private to that component and defines the mutations a component undergoes, typically causing re-render.
State
let QuestionList = React.createClass({ getInitialState: { hasAnswer: false },
processAnswer(answer) { this.setState({ hasAnswer: true }); }});
<QuestionList title="Answer these!" />
☞ title is a prop that is passed into the component
☞ hasAnswer is part of the state of the component
State
With React, you know the exact state a component is in at any given point in time, and the UI will reflect that consistently.
With Angular, it is a lot harder to tell what the state of a particular $scope is, depending on ongoing $digest loops, $watch calls, and other magical constructs.
There's more...
☞ Check out http://facebook.github.io/react/
☞ Play around with it
☞ Form your own opinion
Quick look at FanCenter
White-label web app for user engagement with brands
Users complete various activities to earn rewards
From a dev perspective
☞ Heavy data-driven customization of UI
☞ Show this view if this client
☞ Rounded corners for that client
☞ Big views with dynamic subviews
☞ Composition?
We chose Marionette.js
Boilerplate views on top of Backbone to simplify common use cases, eg. ItemView, CollectionView, Layout.
Keep in mind it is late 2012, Backbone.js is the cool kid on the block
It will be at least a year until anyone knows what Angular is
Marionette.js
class HomePage extends Marionette.Layout template: HomePageJadeTemplate
# Marionette.Layout container for subviews regions: dashboardRegion: '#dashboard-region' ...
_initDashboard: => @dashboardView = new DashboardView({...}); @listenTo this, 'show', => @dashboardRegion.show @dashboardView
Backbone / Marionette
☞ Many moving parts
☞ Poor layout/view lifecycle control
☞ Spaghetti events around DOM presence
☞ False separation of concerns
☞ Simple routing of urls to function calls make managing state, nested views, and re-renders a very manual and tedious process.
Custom build tool, a.k.a Crusher
☞ In-house build tool
☞ Developed to facilitate control of custom client implementations on the front-end (white-label)
☞ Each client has its own app.js and app.css files
☞ Built dynamically from a directory structure organized by client id
☞ Leverages Sass $variables to override defaults with custom configuration
Pros
☞ Front-end owns the view (kinda)
☞ Fits well into model of Cascading Style Sheets
☞ Sharing code with minimal duplication
Cons
☞ Maintenance costs of custom build tool
☞ Very (very) slow build time
☞ False separation of concerns
☞ Database still requires knowledge of specific client configuration
☞ Back-end already provides view-driven data
What if we move all client configuration to the server and use React to build components that manage their own data, styling, and templates?
let HomePage = React.createClass({ mixins: [TextKeyMixin, StyleMixin],
statics: { fetchData(params) { return get({ page_data: { text: ['title.home'] }, model_data: { activities: ['id', 'title'] }, style_data: { attributes: ['borderRadius', 'fontSize', 'backgroundColor'] } }); } },
render() { return ( <div id="home-page" className={this.styleData}> <h1>{this.getTextKey('title.home')}</h1> <Activities activities={this.getModelData('activities')} /> </div> ); }});
Testing components is simple too..
import SampleComponent from "src/common/sample_component.jsx";let {TestUtils} = React.addons;
describe('Sample Component', function() { it('should render with no props', function() { let sampleComponent = TestUtils.renderIntoDocument( <SampleComponent /> );
let heading = TestUtils.findRenderedDOMComponentWithTag( sampleComponent, 'h1');
expect(heading.getDOMNode().textContent).to.be.empty(); });});
To conclude
☞ The problem of scaling large applications with magical frameworks is real
☞ If you think you separated your concerns, think again
☞ The web is changing, keeping up is nearly impossible
☞ Ownership is important, how would you define simplicity?
☞ JavaScript is slowly solving its own problems
☞ There is no silver bullet
To conclude
☞ React's approach is to keep it small and contained
☞ It's just a library for building UI using JavaScript
Suddenly, it's no longer 2006.