notoriousb1t

MVP Part 6: ReactJS

For a while I have been working on a project called Model View Pizza. The aim of the project is to both show good examples of popular JavaScript frameworks and also to enhance my knowledge in general.

About ReactJS

ReactJS is a view rendering system developed by Facebook. It assumes the role of translating a JavaScript model into HTML. It is most commonly paired with the state management library, Redux. Although they are normally used together, I will not be using it in this project in order to evaluate it on its own merits. Without further ado, I'll begin!

About JSX

ReactJS was built to be used with a language named JSX. JSX is a superset of JavaScript that allows you to mix in React elements. A React element is a placeholder for a normal HTML element.

React can outperform other frameworks in certain cases because it limits communication with the HTML in the page. Interacting with the HTML in the page is one of the most resource/time intensive things you can do in the browser. React needs these placeholder or virtual elements to keep track of the things it needs to do to the actual HTML elements.

JSX allows me to write HTML like markup next to my JavaScript, and it translates it to React elements. Here is an example:

JSX -> JavaScript

var div = (<div className="my-class">Some Text</div>);  
var div = React.createElement(  
  "div",
  { className: "my-class" },
  "Some Text"
);

In the example, it translated what looks like a div with a class and some text into a call to React.createElement(). The first argument is the type of element. The second is an object literal with each attribute name as the property name and each attribute value as the property value. ReactJS requires me to use className instead of class, but I don't know why at this point. The third (and each subsequent) argument is a child element or text node.

A Simple Component

I started with the goal of creating a simple example for ReactJS. On one hand, it is a non-trivial amount of code for a simple form submit and display example. On the other, I have really fine grained control over the state I am managing and rendering. Here a component that takes a name from a user and on click shows it below the submit button:

var MyComponent = React.createClass({  
  getInitialState() {
    return {
      name: '',
      message: ''
    };
  },
  onChangeName(e) {
    this.setState({ name: e.target.value });
  },
  onFormSubmit(e) {
    e.preventDefault();
    this.setState({ message: this.state.name });
  },
  render() {
    return (
      <form onSubmit={this.onFormSubmit}>
        <div className="form-group">
          <label for="name">Name</label>
          <input 
            onChange={this.onChangeName} 
            value={this.state.name} 
            id="name" 
            type="text" 
            className="form-control" />
        </div>
        <button className="btn btn-success">Submit</button>
        <p>{this.state.message}</p>
      </form>
    );
  }
});

React.render(<MyComponent />, document.getElementById('app'));  

There is a lot happening here, so I will break it down.

React.createClass()

Each ReactJS component is created by calling React.createClass and passing an object with construction options. There are several built-in methods it expects me to pass in.

getInitialState()

This property tells ReactJS what my initial state should be. Each component can have properties passed in from a parent. I can access them with this.props in this stage to get event handlers or data from the parent. I don't need to get anything from a parent, so I am just returning a default state of no name, no message.

onChangeName() & onFormSubmit()

These properties are not built-in. Any custom event handlers can be set as functions. onChangeName receives the change event fired from the name input below and grabs the current value of the html element assigned to e.target. I am passing in an object with this value assigned to "name" which ReactJS will merge into its this.state property. onFormSubmit does the same thing as for the message property and also prevents the form's default action.

render()

This property is how ReactJS creates or updates HTML. Each time I change the state, ReactJS will call this and compare the last result of this function with this result. It applies the individual differences to the actual HTML in the page. I put {} around the names of functions or properties to tell React that those things are part of the React class.

React.render()

I am calling this to put my component into the page. React.render() expects an instance of my React component and a reference to the HTML element I want to add it into.

Components, Components, Components!

I can build a complex application in ReactJS by composing together different components. Here is an example of how components can interact:

class ButtonComponent extends React.Component {  
  render() {
    return (<button onClick={this.props.onClicked}>   
      {this.props.text}
    </button>)
  }
}

var ContainerComponent = React.createClass({  
  getInitialState() {
    return { message: '', buttonText: 'Click Me!' };
  },
  onButtonClick() {
    this.setState({ buttonText: 'clicked!' });
  },
  render() {
    return (<div>
        <ButtonComponent 
             onClicked={this.onButtonClick} 
             text={this.state.buttonText} />
      </div>);
  }
});

React.render(<ContainerComponent />, document.getElementById('app'));  

ContainerComponent defines a ButtonComponent and passes an onClick function and text through its attributes. When the ButtonComponent is clicked, the ContainerComponent updates the text.

ButtonComponent accesses its attributes through this.props. It can't update the ContainerComponent's state, but it can read them as properties. I defined ButtonComponent using the ES6 Component, but it has some differences from React.createClass that warrant a separate post.

That took a long time to explain, and I think it could be kind of overwhelming for someone new to web programming.

Building the Project

I started out by getting babel compiling and adding a root component named AppComponent. I also added the React.render function to add it to my #view component on the page.

 var AppComponent = React.createClass({
     render() {
         return (<div></div>)
     }
});

React.render(<AppComponent />, document.getElementById('view'))  

Since I had already built this project with several other frameworks, I copied the html from another project and inserted it into the render function.

Next I created a generic Select component to handle all of my select lists and wired up the individual select lists to change functions.

var Select = React.createClass({  
    render() {
        var options = this.props.options.map(p => <option value={p}>{p}</option>);
        return (<select className="ddl" value={this.props.value} onChange={this.props.onChange}>
            {options}
        </select>);
    }
});

 /* in the render function: */
<Select options={this.state.options.sizes} value={this.state.size} onChange={this.onSizeChange} />

/* as part of the React class */
onSizeChange(e) {  
    this.setState({ size: e.target.value });
},

I did that for all of the select lists and also created a checkbox list to handle selecting the toppings:

var CheckBoxList = React.createClass({  
    render() {
        return (<dl className="checkboxlist-inline">
            {this.props.options.map(option => (
                <dd className="checkbox">
                    <label>
                        <input type="checkbox" data-name={option} checked={this.props.value.indexOf(option) !== -1} onClick={this.props.onChange} />
                        <span>{option}</span>
                    </label>
                </dd>
            ))}
        </dl>);
    }
});

To handle switching between each step, I used an Immediately Invoked Function Expression to choose the right React elements during each stage of building the pizza.

<div className="pane level-3">  
    {(() => {
        switch (this.state.step) {
            case steps.start:
                return (<div> /* start pane */</div>);
            case steps.sauce:
                eturn (<div> /* sauce pane */</div>);
            case steps.toppings:
                return (<CheckBoxList options={options.toppings} value={this.state.toppingSelections} onChange={this.onToppingChange} />);
            case steps.done:
                return (<div className="text-center"> /* done pane */</div>);
            default:
                return "";
        }
    })()}
    /* */
</div>  

I had to look that one up. It wasn't obvious as a first time React author that I could do that. However, after that I knew I can use IFFEs to run code in the middle of render function, it solved a lot of odd cases.

Impression

ReactJS has left me with an overall good impression. I think there are tradeoffs compared to other frameworks in terms of developer productivity, but in the end, I could use it to build a powerful, reactive web application.

Check it out on GitHub or see it here on the ModelViewPizza.