Skip to content
This repository has been archived by the owner on Oct 19, 2018. It is now read-only.

Comparing Redux with Hyperloop

Mitch VanDuyn edited this page Jan 17, 2017 · 5 revisions

In trying to find how models and flux stores relate, I was rereading the Redux tutorials. After having been away from that for a while I was amazed clean Hyperloop is compared to the typical JSX code.

For example here is a comparison of Redux TodoMVC and Hyperloop TodoMVC which provide the same Todo UI.

Here are the component code files, which are roughly divided the same way between the two apps.

JS files React/Redux size Hyperloop size Ruby Files
Footer.js 71 29 footer_link.rb, footer.rb
Header.js, MainSection.js 103 25 index.rb
TodoItem.js 65 23 todo_item.rb
TodoTextInput.js 53 20 edit_item.rb
Total 292 97

In addition there are the following "store/action/model" definition files.

JS files React/Redux size Hyperloop size Ruby Files
action/index.js 8
constants/... 9
reducers/todos.js 55
4 models/public/todo.rb
total 72 4
React/Redux Hyperloop
Total 364 101

Note only is the Hyperloop version is less than 1/3 the size, it is persisting and synchronizing the todo list across multiple browsers!

There is nothing wrong with more lines of code, as long as the extra code is adding extra comprehension and making the code easier to maintain. Unfortunately I would say this is not the case.

Let's look at the TodoItem.js (65 SLOC) vs. todo_item.rb (26 SLOC) files.

First there is a preamble in the JS file (4 lines) which does not exist in the ruby file.

import React, { Component, PropTypes } from 'react'
import classnames from 'classnames'
import TodoTextInput from './TodoTextInput'

Then we have the class wrapper which is essentially the same (2 lines) in JS vs Ruby:

export default class TodoItem extends Component {
...
}
class TodoItem < React::Component::Base
...
end

Then we define the properties, and state (11 lines Redux vs 3)

  static propTypes = {
    todo: PropTypes.object.isRequired,
    editTodo: PropTypes.func.isRequired,
    deleteTodo: PropTypes.func.isRequired,
    completeTodo: PropTypes.func.isRequired
  }

  state = {
    editing: false
  }
    param :todo, type: Todo
    define_state editing: false

The JS version is simply more verbose. To define a state in JS is 3 lines compared to 1 in HyperReact. In addition the JS code has an additional 2 declarations for the deleteTodo and completeTodo params. Because Hyperloop uses ActiveRecord methods like delete, and the complete accessor are built into the Todo model no extra charge.

In the JS file we now have 2 helper methods (13 SLOC) defined which don't exist in the Ruby version:

  handleDoubleClick = () => {
    this.setState({ editing: true })
  }

  handleSave = (id, text) => {
    if (text.length === 0) {
      this.props.deleteTodo(id)
    } else {
      this.props.editTodo(id, text)
    }
    this.setState({ editing: false })
  }

These methods are defined in blocks directly in the Ruby code, so there is a bit of a stylistic choice here. If we had pulled them out as methods handleDoubleClick would also be three lines long, but handleSave would only be four lines, as once again ActiveRecord is going to make handling the Todo's internal state easier.

Finally we get to the render method. In React/Redux it looks like this:

  render() {
    const { todo, completeTodo, deleteTodo } = this.props

    let element
    if (this.state.editing) {
      element = (
        <TodoTextInput text={todo.text}
                       editing={this.state.editing}
                       onSave={(text) => this.handleSave(todo.id, text)} />
      )
    } else {
      element = (
        <div className="view">
          <input className="toggle"
                 type="checkbox"
                 checked={todo.completed}
                 onChange={() => completeTodo(todo.id)} />
          <label onDoubleClick={this.handleDoubleClick}>
            {todo.text}
          </label>
          <button className="destroy"
                  onClick={() => deleteTodo(todo.id)} />
        </div>
      )
    }

    return (
      <li className={classnames({
        completed: todo.completed,
        editing: this.state.editing
      })}>
        {element}
      </li>
    )
  }

Before we look at the details of these 34 lines there are some JS statements which are simply not needed in Ruby, and which really clutter up reading the code. These are:

    const { todo, completeTodo, deleteTodo } = this.props

    let element
    ...
      element = (
      ...
      )
      ...
      element = (
      ...
      )
    ...
    return (
      ...
    )

These 8 lines which are almost 25% of the JS render method, add very little clarity to the method. What do these 8 lines do?

First we reassign the props to a intermediate constants (presumably to save a little time, and to make it so we can shorten this.props[:todo] to just todo. In Hyperloop you access props more directly using the params object which takes care of accessing and caching the property, so you would say params.todo. A note: originally you could just say todo without the params. prefix, but it was determined that made the code harder to read. So this behavior is being deprecated. A case where more typing is helpful.

Then I (for stylistic reasons I assume) we compute the child of the li element before actually generating the element. Perhaps the mix of JSX and JS code would quickly get confusing if nested too deeply.

Finally, you have to wrap the whole thing in a return statement, which is just an artifact of JS.

Basically what I see happening here is that JS/JSX is more verbose, so in order to add comprehension, the flow of the code is broken up, methods are added, and intermediate values are introduced. The result is a snowball effect.

Here is complete ruby class for comparison.

class TodoItem < React::Component::Base

  param :todo, type: Todo
  define_state editing: false

  render(LI, class: 'todo-item') do
    if state.editing
      EditItem(todo: todo).
      on(:save) do
        todo.delete if todo.text.blank?
        state.editing! false
      end.
      on(:cancel) { state.editing! false }
    else
      INPUT(class: :toggle, type: :checkbox, checked: params.todo.completed).
      on(:click) { params.todo.update(completed: !params.todo.completed }
      LABEL { params.todo.title }.on(:doubleClick) { state.editing! true }
      A(class: :destroy).on(:click) { params.todo.destroy }
    end
  end
end

and here is the JSX class:

import React, { Component, PropTypes } from 'react'
import classnames from 'classnames'

export default class TodoTextInput extends Component {
  static propTypes = {
    onSave: PropTypes.func.isRequired,
    text: PropTypes.string,
    placeholder: PropTypes.string,
    editing: PropTypes.bool,
    newTodo: PropTypes.bool
  }

  state = {
    text: this.props.text || ''
  }

  handleSubmit = e => {
    const text = e.target.value.trim()
    if (e.which === 13) {
      this.props.onSave(text)
      if (this.props.newTodo) {
        this.setState({ text: '' })
      }
    }
  }

  handleChange = e => {
    this.setState({ text: e.target.value })
  }

  handleBlur = e => {
    if (!this.props.newTodo) {
      this.props.onSave(e.target.value)
    }
  }

  render() {
    return (
      <input className={
        classnames({
          edit: this.props.editing,
          'new-todo': this.props.newTodo
        })}
        type="text"
        placeholder={this.props.placeholder}
        autoFocus="true"
        value={this.state.text}
        onBlur={this.handleBlur}
        onChange={this.handleChange}
        onKeyDown={this.handleSubmit} />
    )
  }
}
Clone this wiki locally