React, JSX, and CoffeeScript
React is neat. It's a reimagining of how your DOM relates to your app state that dramatically simplifies code without being an enormous framework*. It also breaks a rule that I've never cared for, where your templates are supposed to be separate from your code.
In React's world, each component (re-)renders its entire virtual DOM on any state change; React then manages updating the on-page DOM based on what changed. This means the rendering code only needs to model the static state of a component, rather than poking around in the DOM to transition between states.
To facilitate this rendering, React includes an optional library/tool called JSX that lets you inline HTML into your render function. The translation is relatively simple. This code:
render: function() {
return <p><a href="foo">bar</a></p>;
}
becomes, post-translation, the not-keyboard-friendly but not hard to understand:
render: function() {
return React.DOM.p(null, React.DOM.a({href:"foo"}, 'bar'));
}
In words, the tag becomes a function call, the attributes become an object passed as the first argument, and the contents of the tag are further parameters to the function.
The nice thing about interleaving code and DOM is that you can use all
the normal JavaScript functionality in your template. Here's another
example from the React docs with an embedded Array.map
:
return (
<ol>
{this.results.map(function(result) {
return <li key={result.id}>{result.text}</li>;
})}
</ol>
);
CoffeeScript instead of JSX
If you're using CoffeeScript, your source code isn't JavaScript to begin with. But turns out that CoffeeScript's flexible syntax makes it relatively painless to use the underlying API directly.
Start with shortening the DOM alias and writing more or less the same
code as above. Also note that you don't need to explicitly return
as the last statement in a function is implictly returned, and that
the function literal syntax for argumentless function is just a bare
->
:
R = React.DOM
# ...
render: ->
R.p(null, R.a({href:'foo'}, 'bar'))
But you can do better. First, CoffeeScript knows to insert the curlies on an object literal because of the embedded colon.
R.p(null, R.a(href:'foo', 'bar'))
And then you can remove the parens by splitting across lines. When providing args to a function, a comma+newline+indent continues the argument list. Much like Python, the visual layout follows the semantic nesting.
R.p null,
R.a href:'foo', 'bar'
In fact, beyond the first argument, the trailing commas are optional
when you have newlines. Here's the same thing again with two links
inside the <p>
:
R.p null,
R.a href:'foo', 'bar' # note omitted comma here
R.a href:'foo2', rel:'nofollow', 'second link'
CoffeeScript also makes every statement into an expression, which is a
familiar feeling coming from functional programming. It means you can
use statement-like keywords like if
and for
on the right hand side
of an equals sign, or even within a block of code like the above.
Here's a translation of the (7-line) <ol>
example from above.
R.ol null,
for result in @results
R.li key:result.id, result.text
There is one final feature of CoffeScript that I find myself using, which is an alternative syntax for object literals. For example, suppose in the above example the "key" attribute needs to be computed from some more complicated expression:
R.ol null,
for result, index in @results
resultKey = doSomeLookup(result, index)
R.li key:resultKey, result.text
The simplification is that, within a curly-braced object literal, entries without a colon use the variable name as the key. The above could be equivalently written:
R.ol null,
for result, index in @results
key = doSomeLookup(result, index)
R.li {key}, result.text
This is particularly useful when the attributes you want to set have
meaningful names — key
is pretty vague, but if you construct an
href
and a className
variable it's pretty clear where they are
going to be used. These can be mixed with normal key-value pairs, too,
like:
href = ...
className = ...
R.li {href, className, rel:'nofollow'}, ...
Putting it all together, here's a larger example, part of an
implementation of an "inline edit" widget. To the user, this widget
is some text with a "change" button to its right, where clicking
on "change" swaps the line of text out for an edit field positioned in
the same place, allowing the user to make a change to the value
directly. (Like how it works in a spreadsheet.) The first branch of
the if
is the widget's initial state; the @edit
function flips on
the @state.editing
flag.
render: ->
if not @state.editing
R.div null,
@props.text
' ' # space between text and button
R.span className:'link-button mini-button', onClick:@edit, 'change'
else
R.div style:{position:'relative'},
R.input
style:{position:'absolute', top:-16, left:-7}
type:'text', ref:'text', defaultValue:@props.text
onKeyUp:@onKey, onBlur:@finishEdit
To get a feel for these rules, you can just experiment and look at the generated JavaScript. Or you can go to coffeescript.org and click the "Try CoffeeScript" tab, where you can enter nonsense expressions there just to experiment with the syntax.
* Though I do wish it was smaller. I wish I could cut all the support for old IE bits and the event handling abstractions.