Tech Notes: React, JSX, and CoffeeScript
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.