March 26, 2016

React.js in the small

I’m a big fan of accessible technologies. I get suspicious whenever I encounter a framework or library that is “not worth it for a small project,” because that is a coded statement that means either “It has a broad scope of features that a small project won’t take advantage of,” or “it trades off simplicity or user experience for performance or other reasons.” Accessibility also helps open-source projects move from obscure to ubiquitous; if jQuery had required a command-line tool to compile it when it came out, I can practically guarantee you wouldn’t have ever heard about it.

With this in mind, I’m a fan of Javascript frameworks that can be used for small projects as well as large ones. I’ve done this before for Angular 1 (Angular 2 seems not to be worth it for a small project), but I just had occasion to try React.js in creating a sarcastic complement to my previous post, and I thought I’d share the experience.

Defining the app

My last post was about how many different ways one can write web apps. To that end, I made an app that randomly selects a language, framework, and datastore and assigns an acronym to that combination.

First Steps with React.js

I’ve never used React in Javascript before; my only experience is with Om and Reagent, so while I may have a head start on the broad strokes, I’m still starting from zero with regards to using it in Javascript.

Even though I’m using Middleman, and unlike the last time, I elected to write this in plain-old-js5, partly because coffeescript seems to be falling out of favor and I need to re-up my neural connections in JS again. I also decided not even to use React’s JSX processing, for better or for worse; all DOM in my app would be hand-programmed in Javascript.

First, I went to the Techempower Benchmarks and started my copy-paste adventure, producing the raw data you can find in this gist. Then, I went and got react and linked it in my index.html. Finally, I started writing a component and mounted it:

var LanguageSelect = React.createClass({
  render: function(){
    var opts = [];
    for(var ii=0; ii<LANGUAGES; ii++){
      opts[ii] = React.createElement(
        "option",
        {value: LANGUAGES[ii]},
        LANGUAGES[ii]
      );
    }

    return React.createElement(
      "select",
      {
          value: LANGUAGES[0],
      },
      opts
    );
  }
});

ReactDOM.render(
  React.createElement(LanguageSelect),
  document.getElementById("app")
);

That was alright for a start. I learned that creating React components involves using React.createClass to define classes, and then render them using React.createElement. That part was straightforward.

Refining things a bit.

Next, I set out to make a generic Select element. I took my LanguageSelect and decided it would receive its list of options as a property (I initally abused initializeState for this before learning a bit more and deciding that state wasn’t needed).

I also decided that my components would live in an App container, where I would store any and all state the application needed (a lesson from working with the clojurescript react libraries). I also decided that passing state up by using onChange events was the way to go.

I made the container first, and (after some experimention) updated my mounting code accordingly:

var App = React.createClass({
    getInitialState: function(){
        return {
            datastore: DATASTORES[0],
            language: LANGUAGES[0],
            framework: FRAMEWORKS_BY_LANGUAGE[LANGUAGES[0]]
        }
    },

    setLanguage: function(e){
        this.setState({
            language: e.target.value,
            framework: FRAMEWORKS_BY_LANGUAGE[e.target.value][0]
        })
    },
    setDatastore: function(e){ this.setState({datastore: e.target.value}); },
    setFramework: function(e){ this.setState({framework: e.target.value}); },

    render: function(){
        var frameworkOptions = FRAMEWORKS_BY_LANGUAGE[this.props.language];
        var langSelect = React.createElement(
            Select,
            {
                options: LANGUAGES,
                value: this.state.language,
                onChange: this.setLanguage
            }
        );
        return React.DOM.div({},
            React.createElement(Select, {
                options: DATASTORES,
                value: this.state.datastore,
                onChange: this.setDatastore
            }),
            langSelect,
            React.createElement(Select, {
                value: this.state.framework,
                options: frameworkOptions,
                onChange: this.setFramework
            }),
        );
    }
});

ReactDOM.render(
  React.createElement(App),
  document.getElementById("app")
);

Then, I wrote a general-purpose Select component:

var Select = React.createClass({
  updateValue: function(e){
    this.props.onChange(e);
  },
  render: function(){
    var opts = [];
    for(var ii=0; ii<this.props.options.length; ii++){
      opts[ii] = React.createElement(
        "option",
        {value: this.props.options[ii]},
        this.props.options[ii]
      );
    }
    return React.createElement(
      "select",
      {
          value: this.props.value,
          onChange: this.props.onChange
      },
      opts
    );
  }
});

Much better! That’s a bit more general.

One thing to know about React is that everything that you render in your app is a component, from the text to the form to the whole thing itself. It’s just a tree of components, in the same way that the DOM is a tree of Nodes. So needed a component to display the initials of the stack, and another one to display those links that I had painstakingly collected:

var VOWELS = "aeiouyAEIOUY";

var isVowel = function(s){
    return VOWELS.indexOf(s) > -1;
};

var InitialDisplay = React.createClass({
    render: function(){
        var d = this.props.datastore[0],
            l = this.props.language[0],
            f = this.props.framework[0],
            comps = [this.props.datastore, this.props.language, this.props.framework],
            initials = [d, l, f];
            spans = [];

        if(isVowel(d)){
            comps = [this.props.language, this.props.datastore, this.props.framework];
            initials = [l, d, f];
        }else if(isVowel(f)){
            comps = [this.props.datastore, this.props.framework, this.props.language];
            initials = [d, f, l];
        }

        for(var ii=0; ii<initials.length; ii++){
            spans[ii] = React.createElement("span", {key: ii}, initials[ii]);
        }

        return React.DOM.div({},
            React.DOM.h2({},
                "Try the ",
                React.DOM.span({className: "initials"}, spans),
                " stack"
            ),
            React.DOM.section({className: "featuring"},
                "Featuring: ",
                React.createElement(Links, {components: comps}),
                React.DOM.p({}, "Get on the trolley and start writing web applications like it's " + (new Date().getYear() + 1900).toString() + " already!")
            )
        );
    }
});

var Links = React.createClass({
    render: function(){
        var links = [];
        for(var ii=0; ii<this.props.components.length; ii++){
            links[ii] = React.DOM.li({},
                React.DOM.a({href: URLS[this.props.components[ii]]},
                    this.props.components[ii]));
        }
        return React.DOM.ul({className: "links"}, links);
    }
});

Here, the InitialDisplay class is responsible for displaying the initials for the selected datastore, language, and framework, which are passed in as props. It hs a little bit of logic to attempt to put a vowel in the middle of the acronym if possible, too. It, in turn, renders the Links component, which just displays each framework/language/datastore as an a element with its url as the href.

Finally, I just had to install some randomization machinery, and update the App to display all that stuff I just wrote:


var pickRandom = function(coll){
    return coll[Math.floor(Math.random() * coll.length)];
};

var generateRandomState = function(){
    language = pickRandom(LANGUAGES);
    return {
        datastore: pickRandom(DATASTORES),
        language: language,
        framework: pickRandom(FRAMEWORKS_BY_LANGUAGE[language])
    }

}


var App = React.createClass({
    getInitialState: function(){
        return generateRandomState();
    },
    setLanguage: function(e){
        this.setState({
            language: e.target.value,
            framework: pickRandom(FRAMEWORKS_BY_LANGUAGE[e.target.value])
        })
    },
    setDatastore: function(e){ this.setState({datastore: e.target.value}); },
    setFramework: function(e){ this.setState({framework: e.target.value}); },
    randomize: function(){
        this.setState(generateRandomState());
    },
    render: function(){
        var frameworkOptions = FRAMEWORKS_BY_LANGUAGE[this.state.language];
        var langSelect = React.createElement(
            Select,
            {
                options: LANGUAGES,
                value: this.state.language,
                onChange: this.setLanguage
            }
        );
        return React.DOM.div({},
            React.createElement(InitialDisplay, {
                datastore: this.state.datastore,
                language: this.state.language,
                framework: this.state.framework
            }),
            React.DOM.hr(),
            React.DOM.div({className: "selects"},
                React.createElement(Select, {
                    options: DATASTORES,
                    value: this.state.datastore,
                    onChange: this.setDatastore
                }),
                langSelect,
                React.createElement(Select, {
                    value: this.state.framework,
                    options: frameworkOptions,
                    onChange: this.setFramework
                }),
                " or, ",
                React.DOM.a({onClick: this.randomize, href: "#"},
                    "generate a new random stack"
                )
            )
        );
    }
});

That’s it! The whole thing. Again, here’s the whole app in gist form

Impressions

First off, writing DOM in javascript is not a thing that scales well, and IMO jsx only helps this a bit. When all your layout markup is scattered around in the render methods of your components classes, you have a bizarre distribution of display code scattered about.(which best practices usually demand should be as close together as possible). In Clojurescript, I use kioo to deal with this, but it’s not an option for React.

That’s just a gut impression, for the purposes of this small example React’s way is fine. Beyond that and the general gnarliness of Javascript (especially JS5), it really wasn’t too bad. If there existed a loop comprehension syntax I think most of the code would have been calls to React.DOM and React.createElement.

Overall, I give React.js a pass to use for a small project like this. I might try out coffee-react for it though.

Epilogue

Here’s the Select that could have been, had I decided to use coffee-react:

Select = React.createClass
  render: ->
    <select value={@props.value} onChange={@props.onChange}>
      {<option key={v} value={v}>{v}<option> for v in @props.options}
    </select>

Why do I hurt myself like this?!