MobX это простое, опробованное в бою решение для управления состоянием вашего приложения. Этот туториал научит вас основным концептам MobX. MobX это автономная библиотека, но большинство используют ее в связке с React и этот туториал будет сфокусирован на этой комбинации.
Основная идея
Состояние (state ориг.) это сердце каждого приложения и нет более быстрого способа создания забагованого, неуправляемого приложения, как отсутствие консистентности состояния. Или состояние, которое несогласованно с локальными переменными вокруг. Поэтому множество решений по управлению состоянием пытаются ограничить способы, которыми можно его изменять, например сделать состояние неизменяемым. Но это порождает новые проблемы, данные нуждаются в нормализации, нет гарантии ссылочной целостности и становится почти невозможно использовать такие мощные концепты как прототипы(prototypes ориг.).
MobX позволяет сделать управление состоянием вновь простым, вернувшись к корню проблемы: он делает невозможным инконсистентность состояния. Стратегия достижения этого довольно проста: убедится что, все что может быть вынуто из состояния, будет вынуто. Автоматически.
Концептуально MobX обрабатывает ваше приложение как электронная таблица (отсылка к офисной программе для работы с таблицами прим. пер.).
![MobX Cycle](https://habrastorage.org/getpro/habr/post_images/c18/8c8/4fd/c188c84fddfc6139cf4aa603ef10e927.png)
Во-первых, есть состояние State приложения. Графы объектов, массивов, примитивов, ссылок которые формируют модель вашего приложения.
Во-вторых есть производные Derivations. Обычно, это любое значение, которое может быть вычислено автоматически из данных состояния вашего приложения.
Реакции Reactions очень похожи на производные Derivations. Основное отличие: они не возвращают значение, но запускаются автоматически, чтобы выполнить какую то работу. Обычно это связано с I/O. Они проверяют, что DOM обновился или сетевые запросы выполнились вовремя
Наконец, есть действия Actions. Действия это все те штуки которые меняют состояние. MobX проследит, чтобы все изменения в состоянии приложения, вызванные действиями, автоматически обработались всеми производными и реакциями. Синхронно и без помех.
Простой todo store...
Довольно теории, рассмотрим его в действии, будет намного понятнее, чем внимательно читать написанное выше. Ради оригинальности давайте начнем с очень простого Todo хранилища. Ниже приведен очень простой TodoStore, который управляет коллекцией todo. MobX пока не участвует.
import shortid from 'shortid';
class TodoStore {
todos = [];
get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
report() {
if (this.todos.length === 0)
return "<none>";
return `Next todo: "${this.todos[0].task}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}
addTodo(task) {
this.todos.push({
id: shortid.generate(),
task: task,
completed: false,
assignee: null
});
}
}
const todoStore = new TodoStore();
Мы только что создали todoStore
инстанс с коллекцией todos. Теперь надо заполнить todoStore
какими-нибудь объектами. Чтобы убедиться, что от наших изменений есть эффект, мы вызываем todoStore.report
после каждого изменения:
todoStore.addTodo("read MobX tutorial");
console.log(todoStore.report());
todoStore.addTodo("try MobX");
console.log(todoStore.report());
todoStore.todos[0].completed = true;
console.log(todoStore.report());
todoStore.todos[1].task = "try MobX in own project";
console.log(todoStore.report());
todoStore.todos[0].task = "grok MobX tutorial";
console.log(todoStore.report());
Становимся реактивными
До сих пор в нашем коде не было ничего необычного. Но что если мы не хотим вызывать report
явно, но объявим что нужно вызывать этот метод на каждое изменение состояния? Это освободит нас от обязанности вызывать этот метод в нашем коде. Мы должны быть уверены в том, что последний результат вызова report
будет выведен на экран. Но мы не хотим беспокоится, каким образом это будет сделано.
К счастью, именно MobX может сделать это за нас. Автоматически вызывать код, который зависит от состояния. Так что наша функция report
будет вызываться автоматически. Чтобы этого достичь TodoStore
нужно стать отслеживаемым (observable ориг.), чтобы MobX смог следить за всеми изменениями. Давайте немного изменим наш класс.
Так же, свойство completedTodosCount
будет вычислено автоматически из свойства todos
. Мы можем достичь этого используя @observable
и @computed
декораторы.
import shortid from 'shortid';
class ObservableTodoStore {
@observable todos = [];
@observable pendingRequests = 0;
constructor() {
mobx.autorun(() => console.log(this.report));
}
@computed get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
@computed get report() {
if (this.todos.length === 0)
return "<none>";
return `Next todo: "${this.todos[0].task}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}
addTodo(task) {
this.todos.push({
id: shortid.generate(),
task: task,
completed: false,
assignee: null
});
}
}
const observableTodoStore = new ObservableTodoStore();
Вот и все! Мы пометили некоторые свойства как @observable
чтобы MobX знал что они могут изменяться со временем. Расчеты помечены @computed
декораторами, чтобы знать что они могут быть вычислены на основе состояния.
Свойство pendingRequests
и assignee
еще не используются, но мы увидим их в действии чуть ниже. Для краткости, все примеры используют ES6, JSX и декораторы. Но не беспокойтесь, все декораторы в MobX имеют ES5 аналоги.
В конструкторе класса мы создали маленькую функцию, которая выводит отчет, и обернули ее в autorun
. Он создаст реакцию, которая запустится единожды, и после этого будет автоматически перезапускаться всякий раз, когда отслеживаемые данные внутри функции изменятся. Поскольку report
использует отслеживаемое свойство todos
, то он будет выводить результат report
по мере необходимости.
observableTodoStore.addTodo("read MobX tutorial");
observableTodoStore.addTodo("try MobX");
observableTodoStore.todos[0].completed = true;
observableTodoStore.todos[1].task = "try MobX in own project";
observableTodoStore.todos[0].task = "grok MobX tutorial";
Круто, не правда ли? report
вызывается автоматически, синхронно, без утечки промежуточных значений. Если внимательно изучить вывод в лог, вы увидите, что четвертая строка в коде не приведет к новой записи в лог. Потому что report
фактически не изменился в результате переименования таска, но данные внутри изменились. С другой стороны, изменение атрибута name
у первого todo
обновило результат вывода report
, так как name
активно используется в выводе результата report
. Это демонстрирует, что отслеживается не только массив todos
, но и индивидуальные значения в нем.
Делаем React реактивным
Хорошо, до сих пор мы делали реактивными "глупые" отчеты. Теперь настало время сделать реактивный интерфейс вокруг того же хранилища. Компоненты у React (не смотря на свое название), не реактивные из коробки. @observer
декоратор из пакета mobx-react
исправляет это, оборачивая render
метод в autorun
, автоматически делая ваши компоненты синхронизированными с состоянием. Это концептуально ничем не отличается от того что мы делали с report
до этого.
Следующий листинг определяет несколько React компонентов. От MobX здесь только @observer
декоратор. Этого достаточно, чтобы убедится, что каждый компонент перерисовывается, когда изменяются релевантные для него данные. Вам больше не нужно вызывать setState
, и вам не нужно выяснять, как подписываться на части вашего приложения используя селекторы или компоненты высокого порядка (привет Redux), которые нуждаются в конфигурировании. В основном, все компоненты становятся "умными". Если они не определены в "тупой" декларативной манере.
@observer
class TodoList extends React.Component {
render() {
const store = this.props.store;
return (
<div>
{ store.report }
<ul>
{ store.todos.map(
(todo, idx) => <TodoView todo={ todo } key={ todo.id } />
) }
</ul>
{ store.pendingRequests > 0 ? <marquee>Loading...</marquee> : null }
<button onClick={ this.onNewTodo }>New Todo</button>
<small> (double-click a todo to edit)</small>
<RenderCounter />
</div>
);
}
onNewTodo = () => {
this.props.store.addTodo(prompt('Enter a new todo:','coffee plz'));
}
}
class TodoView extends React.Component {
render() {
const todo = this.props.todo;
return (
<li onDoubleClick={ this.onRename }>
<input
type='checkbox'
checked={ todo.completed }
onChange={ this.onToggleCompleted }
/>
{ todo.task }
{ todo.assignee
? <small>{ todo.assignee.name }</small>
: null
}
<RenderCounter />
</li>
);
}
onToggleCompleted = () => {
const todo = this.props.todo;
todo.completed = !todo.completed;
}
onRename = () => {
const todo = this.props.todo;
todo.task = prompt('Task name', todo.task) || "";
}
}
ReactDOM.render(
<TodoList store={ observableTodoStore } />,
document.getElementById('reactjs-app')
);
Следующий листинг показывает, что мы просто должны изменить наши данные. MobX автоматически вычислит и обновит соответствующие части вашего пользовательского интерфейса из состояния в вашем хранилище.
const store = observableTodoStore;
store.todos[0].completed = !store.todos[0].completed;
store.todos[1].task = "Random todo " + Math.random();
store.todos.push({ task: "Find a fine cheese", completed: true });
// etc etc.. add your own statements here...
Работа со ссылками
До сих пор мы создавали отслеживаемые объекты (с прототипом и без), массивы и примитивы. Но вам может показаться интересным, как обрабатываются ссылки в MobX? В предыдущих листингах, вы могли заметить assignee
свойство у todos
. Давайте дадим ему некоторое другое значение, создав еще одно хранилище (ладно, это просто массив) содержащее людей, и назначим их к задачам.
var peopleStore = mobx.observable([
{ name: "Michel" },
{ name: "Me" }
]);
observableTodoStore.todos[0].assignee = peopleStore[0];
observableTodoStore.todos[1].assignee = peopleStore[1];
peopleStore[0].name = "Michel Weststrate";
Теперь у нас есть два независимых хранилища. Одно с людьми, другое с задачами. Чтобы назначить свойству assignee
персону из хранилища с персонами, нам нужно просто присвоить значение через ссылку. Эти значения подхватятся TodoView
автоматически. С MobX нет нужды в нормализации данных и написании селекторов, чтобы наши компоненты обновлялись. На самом деле, не имеет значения где хранятся данные. Пока объекты "наблюдаемы", MobX будет отслеживать их. Настоящие JavaScript ссылки тоже работают. MobX отслеживает их автоматически если они релевантны для производных значений.
Асинхронные действия
Так как все в нашем маленьком todo
приложении является производным от состояния, то не имеет значения где это состояние будет изменено. Это позволяет достаточно просто создавать асинхронные действия.
Мы начинаем с обновления свойства pendingRequests
, чтобы интерфейс отобразил текущий статус загрузки. После завершения загрузки, мы обновим список todo
и уменьшим счетчик pendingRequests
. Просто сравните этот кусок кода с тем, что мы видели выше, чтобы увидеть как используется свойство pendingRequests
.
observableTodoStore.pendingRequests++;
setTimeout(function() {
observableTodoStore.addTodo('Random Todo ' + Math.random());
observableTodoStore.pendingRequests--;
}, 2000);
DevTools
Пакет mobx-react-devtools
предоставляет инструментарий разработчика, который может быть использован в любом MobX + React приложении.
Вывод
На этом все! Никакого бойлерплейта. Простые и декларативные компоненты которые формируют UI легко и просто. Полностью обновляются из состояния. Теперь вы готовы начать использовать пакеты mobx
и mobx-react
в вашем приложении.
Краткое резюме вещей которые вы сегодня узнали:
- Используйте
@observable
декоратор илиobservable(объект или массив)
функцию чтобы сделать ваши объекты отслежываемыми MobX - Декоратор
@computed
может быть использован для создания функций которые вычисляют свое значение из состояния - Используйте
autorun
, чтобы автоматически запускать ваши функции на основе отслеживаемого состояния. Это применимо для логирования или сетевых запросов. - Используйте декоратор
@observer
из пакетаmobx-react
, чтобы наделить ваши React компоненты реактивной силой. Они автоматически будут наиболее эффективно обновляться. Даже в больших и сложных приложениях с большим количеством данных.
MobX не контейнер состояния
Люди часто используют MobX как альтернативу Redux. Но пожалуйста, обратите внимание, что это просто библиотека, для решения определенной проблемы а не архитектура или контейнер состояния. В этом смысле приведенные выше примеры являются надуманными и рекомендуется использовать правильные архитектурные решения, как инкапсуляция логики в методах, их организация в хранилищах или контроллерах и т.д. Или как кто то написал на Hacker News:
«Использовать MobX означает использование контроллеров, диспетчеров, действий, супервизоров или любой другой формы управления потоком данных, это ведет нас к тому, что архитектурную потребность вашего приложения проектируете вы сами, а не используя то что используют по умолчанию для чего то большего чем Todo приложение»
Еще
Заинтригованы? Вот некоторые полезные ссылки (на английском прим. пер.):
- MobX на GitHub
- Api документация
- (Блогозапись) Making React reactive: the pursuit of high performing, easily maintainable React apps
- (Блогозапись) Becoming fully reactive: an in-depth explanation of Mobservable
- JSFiddle c простым todo приложением
- MobX, React, TypeScript boilerplate
- MobX, React, Babel boilerplate
- MobX демо с конференции Reactive2015
- MobX + React TodoMVC