Пишемо просте додаток на React з використанням бібліотеки cellx

Ідея написання статті з'явилася в цій гілці, може комусь буде цікаво її почитати. Відразу скажу, письменник (в тому числі коду) з мене так собі, але я буду старатися.
Писати будемо як завжди тудулист, набрид звичайно до чортиків, але щось краще для демонстрації придумати складно. Відразу посилання на працююче додаток: жмяк код).

Дані програми
І відразу в бій, почнемо з сховища. Єдиний тип необхідний для цього додатка — Todo:
import { EventEmitter } from 'cellx';
import { вами } from 'cellx-decorators';

export default class Todo extends EventEmitter {
@вами text = void 0;
@вами 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 { вами, computed } from 'cellx-decorators';
import All from './types/Todo';

class Store extends EventEmitter {
@вами 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 (<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. Грубо кажучи, він просто робить метод
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 (<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 зростає лінійно, без нього — зазвичай немає і в якийсь момент приходимо до мішанині подій в якій без півлітра не розібратися. Для вирішення проблеми, крім реактивного програмування, є й інші підходи зі своїми плюсами і мінусами, але їх порівняння — вже інша історія (якщо коротко, як мінімум, вони програють з-за великої кількості зайвих обчислень і, як результат, більш низької продуктивності).
загалом за кодом це все, ще раз посилання на результат: жмяк код).
Порівняння з іншими бібліотеками
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 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.