In this post we will implement a basic to-do list app using React and cellx. Cellx is a library that offers an ultra-fast implementation of reactivity for JavaScript. And React, you all know it :-)
TL;DR Here is the final result: live version(code)
Data Models
Let’s start this app with definition of data models. The only data type we need in this app is Todo
:
import { EventEmitter } from 'cellx';
import { observable } from 'cellx-decorators';
export default class Todo extends EventEmitter {
@observable text = void 0;
@observable done = void 0;
constructor(text, done = false) {
super();
this.text = text;
this.done = done;
}
}
In this example everything is simple. There are a couple of observable fields: one holds the description of the task and another one the state of the task. Extending from cellx.EventEmitter
is necessary for subscribing to change events:
todo.on('change:text', () => {/* ... */});
Now we are ready to create the main data store that will hold a collection of Todo
items:
import { EventEmitter, cellx } from 'cellx';
import { observable, computed } from 'cellx-decorators';
import Todo from './types/Todo';
class Store extends EventEmitter {
@observable todos = cellx.list([
new Todo('Primum', true),
new Todo('Secundo'),
new Todo('Tertium')
]);
@computed doneTodos = function() {
return this.todos.filter(todo => todo.done);
};
}
export default new Store();
At this point things get more interesting. Here we use cellx.list
(alias for new cellx.ObservableList
) inside a class that extends cellx.EventEmitter
. Since the list is observed a change event is emitted by the Store whenever the list changes. How does the list detect if there were any changes inside its elements? The list automatically subscribes to change
events of its items if the items extend cellx.EventEmitter
as well. This allows you to create your own collections and make them compatible by extending cellx.EventEmitter
. Cellx offers two built-in collections — cellx.list
and cellx.map
. Also indexed versions of both collections are available.
Also the Store uses a new decorator called computed
. It decorates a function that computes the value for the property doneTodos
. You don’t need to care how it works under the hood but cellx
ensures that the value will be up-to-date in an optimal way.
The View Layer
Now we are ready to display the data from the Store
. First, the view for store items:
import { observer } from 'cellx-react';
import React from 'react';
import toggleTodo from '../../actions/toggleTodo';
import removeTodo from '../../actions/removeTodo';
@observer
export default class TodoView extends React.Component {
render() {
let todo = this.props.todo;
return (<li>
<input type="checkbox" checked={ todo.done } onChange={ this.onCbDoneChange.bind(this) } />
<span>{ todo.text }</span>
<button onClick={ this.onBtnRemoveClick.bind(this) }>remove</button>
</li>);
}
onCbDoneChange() {
toggleTodo(this.props.todo);
}
onBtnRemoveClick() {
removeTodo(this.props.todo);
}
}
Here we use a class decorator called observer
. Basically, it makes a computed cell out of the method render
and calls React.Component#forceUpdate
whenever the cell is changed.
And the top-level view looks as follows:
import { computed } from 'cellx-decorators';
import { observer } from 'cellx-react';
import React from 'react';
import store from '../../store';
import addTodo from '../../actions/addTodo';
import TodoView from '../TodoView';
@observer
export default class TodoApp extends React.Component {
@computed nextNumber = function() {
return store.todos.length + 1;
};
@computed leftCount = function() {
return store.todos.length - store.doneTodos.length;
};
render() {
return (<div>
<form onSubmit={ this.onNewTodoFormSubmit.bind(this) }>
<input ref={ input => this.newTodoInput = input } />
<button type="submit">Add #{ this.nextNumber }</button>
</form>
<div>
All: { store.todos.length },
Done: { store.doneTodos.length },
Left: { this.leftCount }
</div>
<ul>{
store.todos.map(todo => <TodoView key={ todo.text } todo={ todo } />)
}</ul>
</div>);
}
onNewTodoFormSubmit(evt) {
evt.preventDefault();
let newTodoInput = this.newTodoInput;
addTodo(newTodoInput.value);
newTodoInput.value = '';
newTodoInput.focus();
}
}
There are a couple of new computed attributes in this view and unlike the previously shown examples they use foreign objects to compute the value. Cellx imposes no restrictions on this. nextNumber
and leftCount
could be easily moved to Store
. It is better to keep computed attributes where they belong to. So I would move leftCount
to the store because it can be potentially re-used elsewhere and I would keep nextNumber
in this view.
App Logic
Cellx is not used in actions and, therefore, I have simplified them drastically. It is not event a Flux now, rather some MVC in terms of Flux. I hope you are fine with this simplification.
Result
In this case the app is very simple and it can be easily written without cellx. But if you work on a more complex app with more complex interdependencies cellx will be very helpful. Otherwise, it is very hard to understand data flows inside the app.
So here is the working result and the source code live version(code)
Comparison to other libraries
MobX
I am often asked how it is different from MobX. This is the most closed alternative and there are not so many differences:
- cellx is about 10x faster
- the cells are a bit more powerful due to
pull
/put
methods which allow synchronizing with sync stores as well as with async stores. MobX does not offer something like this. - MoBX integrates better with React and unlike Cellx requires more tight integration with the business logic of your app.
Kefir.js, Bacon.js
Differences to these libraries are more significant. The performance difference is even bigger but the more important difference is how the cells are created. Cellx allows defining cells as functions. For example,
var val2 = cellx(() => val() + 1);
Whereas these libraries offer something like this (pseudo-code):
var val2 = val.lift(add(1));
So their advantage is that the code easier to read and it’s more nice. But the disadvantage is that you have to memorize all the methods in order to use the library efficiently.
So compared to these libraries cellx is a more low-level library. Using cellx you can implement methods found in Kefir.js and Bacon.js.
Follow the development of cellx on Github: https://github.com/Riim/cellx