[Из песочницы] Пишем простое приложение на React с использованием библиотеки cellx

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


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



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


И сразу в бой, начнём с хранилища. Единственный тип необходимый для этого приложения — 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 (алиас для new cellx.ObservableList) — наблюдаемый список, наследует от cellx.EventEmitter и при любом своём изменении генерирует событие change. Наблюдаемое поле получая в качестве значения что-то наследующее от cellx.EventEmitter подписывается на его change и тоже изменяется при этом событии. Всё это значит, что не обязательно использовать встроенные коллекции, можно сделать свои унаследовав их от cellx.EventEmitter. Из коробки есть cellx.list и cellx.map. Отдельным модулем есть индексируемые версии обоих коллекций: cellx-indexed-collections.


Ещё один новенький — декоратор computed, вычисляемые поля — это самая суть cellx-a — вы просто пишите формулу вычисляемого поля, вам не нужно самому подписываться на done каждого todo при его добавлении и отписываться от него же при удалении, всё это делает cellx пока вы не видите, вам остаётся расслабиться и получать удовольствие описывая самую суть. При этом описание происходит, можно сказать, в декларативном виде — уже не нужно думать о событиях и о том как изменения будут распространяться по системе, всё пишется так, как будто отработает лишь раз. Кроме того cellx очень умный и автоматически делает некоторые хитрые оптимизации: динамическая актуализация зависимостей и схлопывание и отбрасывание событий не допустят избыточных расчётов и обновлений интерфейса. Если делать всё это вручную, код получается довольно объёмным, но, что намного хуже — глючным. Отладкой же 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 (
  • { todo.text }
  • ); } onCbDoneChange() { toggleTodo(this.props.todo); } onBtnRemoveClick() { removeTodo(this.props.todo); } }

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


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


    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 (
    this.newTodoInput = input } />
    All: { store.todos.length }, Done: { store.doneTodos.length }, Left: { this.leftCount }
      { store.todos.map(todo => ) }
    ); } 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 растёт линейно, без него — обычно нет и в какой-то момент приходим к мешанине событий в которой без поллитра не разобраться. Для решения проблемы, кроме реактивного программирования, есть и другие подходы со своими плюсами и минусами, но их сравнение — уже другая история (если кратко, как минимум они проигрывают из-за большого количества лишних вычислений и, как результат, более низкой производительности).


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


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


    MobX


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


    1. cellx примерно в 10 раз быстрее.
    2. В статье про атомы я подсмотрел методы/опции put и pull, позволяющие ячейкам уметь чуть больше: синхронизация значения с синхронным хранилищем, синхронизация значения с асинхронным хранилищем, про pull. У MobX я ничего похожего не нашёл.
    3. Разная система очистки памяти, у cellx это пассивный режим, в MobX вообще нельзя отписаться от ячейки после подписки, что для меня какая-то странность, когда необходима отписка необходимо использовать autorun, который можно «убить» возвращаемым 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-е есть возможность добавить ячейкам свои методы и ничто не мешает довести его до уровня этих библиотек, можно сказать, что он более низкоуровневый.


    Подвал


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

    Комментарии (0)

    © Habrahabr.ru