Using LiveScript with React

Let me introduce you to a marriage made in heaven. Two beautiful things - React and LiveScript - that work together so well they could have been built for each other.

React components are mostly declarative. But they're written in script rather than a templating language. JavaScript ends up being too messy for this job and so Facebook invented JSX (an XML syntax you can embed into JavaScript). Everyone's first reaction seems to be "yuk"!

This is what it looks like (the examples are taken from the must-read article Thinking in React):

/** @jsx React.DOM */
var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

But see how it looks in LiveScript:

product-category-row = React.create-class do
  render: ->
    tr null,
      td col-span: '2', @props.category

Cool, hey? Clean, to the point and no clutter.

But it gets better...

First with JSX:

/** @jsx React.DOM */
var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

Now with LiveScript:

product-row = React.create-class do
  render: ->
    tr null,
      td null,
        span do
          if @props.product.stocked
            style:
              color: 'red'
          @props.product.name
      td null, @props.product.price

This is much easier to understand and much more declarative. Because everything is an expression, you can put if statements, for loops, anything you want in place of either the props or the children arguments to the component constructor.

OK, so you could do something simliar in CoffeeScript. But you'd miss out on all the amazing extras and functional goodness that LiveScript brings to the table (as well as fixing a whole bunch of CoffeeScript problems such as its scoping. Don't get me started).

But hang on, isn't it a bit weird though. The tr and the td have a null after them and then a comma, but the span has a do and no comma. What exactly are the rules? And where do I pass in the props and where do I add the children?

All React component constructor functions have the same two arguments: initialProps and children. So if we aren't sending in any initial props, we must specify null (or void). That's the first argument to tr. Fortunately for us we can pass in the children as an array or as separate arguments. So the two td components, in the example above, are passed in as the 2nd and 3rd arguments to the tr component. The do simply creates a block to pass as the first argument (in this case).

But passing in arrays works well in LiveScript too. We can use all the functional list manipulations from prelude-ls.

First in JSX:

/** @jsx React.DOM */
var ProductTable = React.createClass({
    render: function() {
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        });
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

Then in LiveScript:

product-table = React.create-class do
  render: ->
    last-category = null
    table null,
      thead null,
        tr null,
          th null, 'Name'
          th null, 'Price'
      tbody null,
        @props.products |> map ->
          if it.category isnt last-category
            product-category-row do
              category: it.category
              key: it.category
          else
            last-category = it.category
            product-row do
              product: it
              key: it.name

I suppose the first thing to note is that you don't have to build up chunks of UI first and then add them later. It's all inline. And that's because we can pass arrays of children as the second argument. So we take the products from the passed-in props and pipe them to the curried map function from prelude-ls. This returns an array into the tbody's second argument.

If the product-row instance, in the example above, had children, you could add them after the props (you can see an example of this in the span inside the product-row component itself). LiveScript is clever enough to know that they don't look like more props and so will pass them as the next argument. Here's a better example:

product-row do
  product: it
  key: it.name
  span do
    class-name: 'child-class'
    "I'm a child of the span, which in turn is a child of the product-row"

It looks beautiful to me. Not unlike Jade. But you get to use all the power of a proper language :-)

By the way, Red Badger is hosting the London React User Group. We already have 71 members and the first meetup will be in mid-June. Please join and come along!

Stuart Harris

Chief Scientist and Founder

More from Stuart