In my continual search for the ever more efficient and pragmatic Javascript UI framework, I've stumbled upon React, but not just React, the special combination of React, Coffeescript, and RequireJS.
JSX is the more elegant way to compose the DOM within React classes, however, it does require an additional compile step, and there is a bit of added complexity when integrating with RequireJS.
It would be hard to give up JSX and go back to building the DOM with plain javascript, the syntax isn't so elegant, and it gets quite busy. Coffeescript, at first thought, may seem like a lesser alternative; I will propose, however, that it may just be equal to more practical than even JSX.
Defining the DOM
Let's take a look at the three ways to write an unordered list in React.
JSX:
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
Javascript:
React.DOM.ul({}, [
React.DOM.li({}, 'Item 1'),
React.DOM.li({}, 'Item 2'),
])
Coffeescript:
(ul {}, [
(li {}, ['Item 1'])
(li {}, ['Item 2'])
])
Coffeescript w/Lodash:
(ul {}, _.map ['Item 1', 'Item 2'], (item) -> (li {}, [item]))
All of these are identical.
The CoffeeScript version displays some lisp-like and jade-like feel to it. Not needing to close the tags feels satisfying, and being directly in code feels good and does open up a more consistent way to embed logic alongside the DOM. In JSX, it isn't much of a burden, but I do see a benefit of going pure CoffeeScript.
By not closing I mean:
(li {}, [])
versus <li></li>
Another example of how much more compact CoffeeScript can be:
JSX:
render: function() {
var text = this.state.liked ? 'like' : 'unlike';
return (
<p onClick={this.handleClick}>
You {text} this. Click to toggle.
</p>
);
}
CoffeeScript:
render: ->
text = if @state.liked then 'like else 'unlike
(p {onClick=@handleClick}, ["You #{text} this. Click to toggle."])
You could further embed the text ternary logic in the #{}, but that gets a bit messy.
Let's unpack what is really going on in CoffeeScript with a (elem {}, [])
(elem {}, [])
would really translate to elem({}, [])
Looking at the Chrome console at the a de-structured element:
function (props, children) {
var instance = new Constructor();
instance.construct.apply(instance, arguments);
return instance;
}
In other words, any element defined through the following construct will be a React.DOM function that takes props and children as arguments:
{ul, li, div, h4} = React.DOM
This is just what you would expect from the regular React.DOM.* way of composing the DOM in javascript. The code above is CoffeeScript's convenient way of de-structuring objects from right to left, this is also proposed in the ECMAScript 6 Specification.
RequireJS with CoffeeScript
The conciseness and compactness of the combination of CoffeeScript and RequireJS are gratifying as a developer. This particular combination has nothing to do with React, but it will greatly augment the cleanness and organization of your project, be it React, or whatnot.
The first order of business here is to define a function to let us mix in any arbitrary properties to any class prototype:
# The code of the include was inspired from https://arcturo.github.io/library/coffeescript/03_classes.html
# Since the default return type is an object, and no return keyword is necessary, this bit of code effectively returns
# an object containing a single 'include' function field. The corresponding js is over double in size.
# See the extend function in the above link for extending objects, not classes.
# Alternatively, see https://coffeescriptcookbook.com/chapters/classes_and_objects/mixins
define 'moduleMixin', [], () ->
include: (obj) ->
for key, value of obj when key not in ['extended', 'included']
# Assign properties to the prototype
@::[key] = value
obj.included?.apply(@)
@
I'm not going to go in-depth into RequireJS other than to mention that it can be syntactically verbose, however it is simplified by CoffeeScript. The basic define function call is:
# All arguments are optional
define module name, [dependencies], (dependency names) ->
# code goes here
Create the Mixins
Now that we have the basic building blocks for mixins, let's mixin.
Define a function to add numbers:
define 'sumMixin', [], () ->
sum: (a, b) -> a + b
And now a function to multiply numbers:
define 'multMixin', [], () ->
mult: (a, b) -> a * b
Simple enough right. Including those in any other defined class is just as easy:
define 'someController', ['moduleMixin', 'sumMixin', 'multMixin'], (Module, SumMixin, MultMixin) ->
class SomeController extends Module
# Make sure the mixin functions and variables are included to 'this' class
@include.call(@, SumMixin)
@include.call(@, MultMixin)
constructor: () -> undefined
The above is saying this. Define a new module named 'someController' (You don't need the name if you want to reference a js file on the filesystem), depending on the given dependencies, and return a class SomeController which mixes in the SumMixin and MultMixin functions.
As you can probably tell at this point, the ability to define and include any mixin opens up a world of possibilities in terms of refactoring your existing javascript code. You can pick and choose which functionality you want to mix into your javascript classes or objects.
Create the React class
The final piece of this example would be defining the React view and injecting the above controller.
require ['someController'], (SomeController) ->
{ul, li, div, h4} = React.DOM
controller = new SomeController()
ExampleView = React.createClass
render: ->
(div {}, [
(h4 {}, ['Requirejs + Coffeescript + React Example'])
(ul {}, [
(li {}, ['Sum of 1 and 2 is: ' + @props.sum(1, 2)]),
(li {}, ['Mult of 5 and 6 is: ' + @props.mult(5, 6)]),
])
])
opts =
sum: controller.sum
mult: controller.mult
React.renderComponent (ExampleView opts), document.body
Note that I'm using require vs define here purely for this example. I am not defining another module, just requiring existing modules. In the real world, you will probably want your React view components also defined as modules so you can require/depend on them in other components. That is a very common model.
The readability of this approach is fantastic, the code is clean, and there is no need for the translated JSX layer. If you haven't tried this combination I would definitely encourage you to do so.
Example Code
For the full example from the above code please see: