-
Notifications
You must be signed in to change notification settings - Fork 18
Comparing Redux with Hyperloop
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} />
)
}
}