- PVSM.RU - https://www.pvsm.ru -

Пишем простое приложение на React с использованием библиотеки cellx

Идея написания статьи появилась в этой ветке [1], может кому-то будет интересно и её почитать. Сразу скажу, писатель (в том числе кода) из меня так себе, но я буду стараться.

Писать будем как обычно тудулист, надоел конечно до чёртиков, но что-то лучшее для демонстрации придумать сложно. Сразу ссылка на работающее приложение: жмяк [2] (код [3]).

Данные приложения

И сразу в бой, начнём с хранилища. Единственный тип необходимый для этого приложения — 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;
    }
}

Тут всё предельно просто, парочка наблюдаемых полей, одно содержит текст задачи, другое статус её выполнения.

Наследование от cellx.EventEmitter необходимо на случай если в дальнейшем понадобится подписаться на изменения какого-то поля:

todo.on('change:text', () => {/* ... */});

В данном приложении такого нет и наследование можно убрать, я просто зачем-то всегда пишу его заранее.

Теперь напишем корневое хранилище:

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();

Здесь уже поинтереснее. Используется cellx.list [4] (алиас для new cellx.ObservableList) — наблюдаемый список, наследует от cellx.EventEmitter и при любом своём изменении генерирует событие change. Наблюдаемое поле получая в качестве значения что-то наследующее от cellx.EventEmitter подписывается на его change и тоже изменяется при этом событии. Всё это значит, что не обязательно использовать встроенные коллекции, можно сделать свои унаследовав их от cellx.EventEmitter. Из коробки есть cellx.list и cellx.map [5]. Отдельным модулем есть индексируемые версии обоих коллекций: cellx-indexed-collections [6].

Ещё один новенький — декоратор computed, вычисляемые поля — это самая суть cellx-a — вы просто пишите формулу вычисляемого поля, вам не нужно самому подписываться на done каждого todo при его добавлении и отписываться от него же при удалении, всё это делает cellx пока вы не видите, вам остаётся расслабиться и получать удовольствие описывая самую суть. При этом описание происходит, можно сказать, в декларативном виде — уже не нужно думать о событиях и о том как изменения будут распространяться по системе, всё пишется так, как будто отработает лишь раз. Кроме того cellx очень умный и автоматически делает некоторые хитрые оптимизации: динамическая актуализация зависимостей [7] и схлопывание и отбрасывание событий [8] не допустят избыточных расчётов и обновлений интерфейса. Если делать всё это вручную, код получается довольно объёмным, но, что намного хуже — глючным. Отладкой же cellx-а заниматься приходиться раз в сто лет, он просто работает.

Представление приложения

Переходим к слою отображения. Сначала компонент задачи:

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);
    }
}

Здесь из новенького — декоратор observer из модуля cellx-react [9]. Грубо говоря, он просто делает метод render вычисляемой ячейкой и вызывает React.Component#forceUpdate [10] при её изменении.

Остаётся корневой компонент приложения:

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();
    }
}

Здесь ещё парочка вычисляемых полей, отличаются от Store#doneTodos они лишь тем, что поля из которых они вычисляются лежат не на текущем экземпляре (this) а где-то в другом месте, cellx никак не ограничивает в этом плане, эти поля можно спокойно переместить в Store и всё так же будет работать. Определять, где должно лежать поле лучше по его сути — если поле специфично для какого-то определённого компонента, то пусть в нём и вычисляется, светиться в общем хранилище ему нет смысла. В данном случае я бы #leftCount перенёс в хранилище, а #nextNumber вполне неплохо смотриться и здесь.

Бизнес-логика приложения

В экшенах cellx никак не используется, поэтому я максимально их упростил, получился даже не Flux, а какой-то MVC в терминах Flux-а. Надеюсь вы мне простите это упрощение.

Результат

В данном случае приложение совсем простое и написать его так же просто можно и без cellx-а (никаких подписок на каждый done здесь не потребуется), при дальнейшем же усложнении связей в приложении сложность их описания на cellx-e растёт линейно, без него — обычно нет и в какой-то момент приходим к мешанине событий в которой без поллитра не разобраться. Для решения проблемы, кроме реактивного программирования, есть и другие подходы со своими плюсами и минусами, но их сравнение — уже другая история (если кратко, как минимум они проигрывают из-за большого количества лишних вычислений и, как результат, более низкой производительности).

В общем-то по коду это всё, ещё раз ссылка на результат: жмяк [2] (код [3]).

Сравнение с другими библиотеками

MobX

Чаще всего спрашивают отличия от MobX [11]. Это наиболее близкий аналог и отличий немного:

  1. cellx примерно в 10 раз быстрее [12].
  2. В статье про атомы [13] я подсмотрел методы/опции put и pull, позволяющие ячейкам уметь чуть больше: синхронизация значения с синхронным хранилищем [14], синхронизация значения с асинхронным хранилищем [15], про pull [16]. У MobX я ничего похожего не нашёл.
  3. Разная система очистки памяти, у cellx это пассивный режим [17], в MobX вообще нельзя отписаться от ячейки после подписки, что для меня какая-то странность, когда необходима отписка необходимо использовать autorun [18], который можно "убить" возвращаемым disposer-ом. Из минусов autorun-а — инициализирующий запуск колбека часто вообще не в тему.
  4. MobX лучше интегрирован с React-ом, в отличии от cellx-а он как-то вклинивается в слой бизнес-логики приложения. Я так и не понял зачем он там, но видимо зачем-то нужен.
  5. У MobX явно лучше с документацией.

Kefir.js, Bacon.js

Тут отличия более существенны. Отставание в скорости ещё больше, но важнее не это. Эти библиотеки предлагают создавать вычисляемые ячейки несколько иначе, в, наверное, более функциональном виде. То, что на cellx-e будет выглядеть так:

var val2 = cellx(() => val() + 1);

На этих библиотеках превратиться в что-то вроде (псевдокод, как там точно я не помню, да и не суть):

var val2 = val.lift(add(1));

Плюс в более красивом, человекочитаемом коде, минус в заметно большем пороге входа, так как теперь нужно запомнить 100500 методов на все случаи жизни (можно конечно обходиться и каким-то минимальным набором).

В тоже время в cellx-е есть возможность добавить ячейкам свои методы и ничто не мешает довести его до уровня этих библиотек, можно сказать, что он более низкоуровневый.

Подвал

Вопросы по библиотеке и идеи по её дальнейшему развитию принимаются на гитхабе [19].
Благодарю за внимание.

Автор: Riim

Источник [20]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/200569

Ссылки в тексте:

[1] этой ветке: https://habrahabr.ru/post/308782/#comment_9779664

[2] жмяк: https://riim.github.io/react-cellx-todo-app/

[3] код: https://github.com/Riim/react-cellx-todo-app

[4] cellx.list: https://github.com/Riim/cellx/blob/master/README.ru.md#cellxobservablelist

[5] cellx.map: https://github.com/Riim/cellx/blob/master/README.ru.md#cellxobservablemap

[6] cellx-indexed-collections: https://github.com/Riim/cellx-indexed-collections

[7] динамическая актуализация зависимостей: https://github.com/Riim/cellx/blob/master/README.ru.md#Динамическая-актуализация-зависимостей

[8] схлопывание и отбрасывание событий: https://github.com/Riim/cellx/blob/master/README.ru.md#Схлопывание-и-отбрасывание-событий

[9] cellx-react: https://github.com/Riim/cellx-react

[10] React.Component#forceUpdate: https://facebook.github.io/react/docs/component-api.html#forceupdate

[11] MobX: https://github.com/mobxjs/mobx

[12] быстрее: https://github.com/Riim/cellx/blob/master/README.ru.md#Тест-производительности

[13] атомы: http://habrahabr.ru/post/235121/

[14] синхронизация значения с синхронным хранилищем: https://github.com/Riim/cellx/blob/master/README.ru.md#Синхронизация-значения-с-синхронным-хранилищем

[15] синхронизация значения с асинхронным хранилищем: https://github.com/Riim/cellx/blob/master/README.ru.md#Синхронизация-значения-с-асинхронным-хранилищем

[16] про pull: https://github.com/Riim/cellx/issues/7#issuecomment-245661130

[17] пассивный режим: https://github.com/Riim/cellx/blob/master/README.ru.md#dispose-или-как-убить-ячейку

[18] autorun: https://mobxjs.github.io/mobx/refguide/autorun.html

[19] гитхабе: https://github.com/Riim/cellx/issues/new

[20] Источник: https://habrahabr.ru/post/313038/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox