By Dmitry Vibe November 10, 2016 8:50 PM
Building a Reactive App using Cellx and React

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:

  1. cellx is about 10x faster
  2. 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.
  3. 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