Найти тему
VK Cloud

Плюсы связки Mobx + React для управления состоянием приложения

Оглавление
React предлагает универсальную систему управления состоянием компонентов посредством свойства компонента this.state и метода this.setState(). Однако по мере роста приложения и увеличения количества вложенных компонентов поддерживать код становится труднее. Расскажу, как решить эту проблему, используя React в связке с Mobx.

Почему React лучше использовать с Mobx

Если использовать только React, состояние приложения может стать неконсистентным. Бизнес-логика размазывается по иерархии компонентов. Требуется писать больше колбэков для передачи на верхний уровень информации о состоянии компонентов. Компоненты приложения становится труднее переиспользовать.

Поэтому для управления состоянием часто используют библиотеки в дополнение к React. Тогда, в терминах описания архитектуры, React в нашем MVC- или MVVM-приложении отвечает за View (отображение) плюс общий каркас приложения. А работа с Model строится с помощью библиотеки управления состоянием. Например, такой как Mobx.

Упрощенная схема работы библиотеки представлена на следующем рисунке:

-2

Я расскажу, как сделать простое приложение, используя связку React + Mobx, и какие преимущества это дает.

При построении приложения примем следующую концепцию: каждый компонент или группа компонентов делятся на «глупые» (только отображение) и «умные» (state, хендлеры, сайд-эффекты). Приложение состоит из вложенных компонентов, уровень вложенности может быть любой. В листингах кода внимание в первую очередь уделяется работе с данными.

Сквозное подключение данных в Mobx

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

import React from "react";
import ReactDOM from "react-dom";
import {Provider} from "mobx-react";
import App from "./components/App";
import mainStore from "./stores/mainStore";
import optionsStore from "./stores/optionsStore";
// для IE11
require("es6-object-assign").polyfill();

const stores = {
mainStore,
optionsStore,
ButtonStore : mainStore.ButtonStore,
FioStore : mainStore.FioStore,
EmailStore : mainStore.EmailStore
};

ReactDOM.render((
<Provider {...stores}>
<App />
</Provider>
), document.getElementById('reactContainer'));

На верхнем уровне мы используем механизм Provider. В его свойствах перечисляются те хранилища, которые будут востребованы в разных компонентах в приложении. Сразу отметим, в примере выше некоторые хранилища представлены как свойства главного хранилища. Подробнее об этом будет сказано в последнем разделе статьи.

Теперь рассмотрим подключение данных в компонентах приложения:

import React from "react";
import {inject, observer} from "mobx-react";

@inject("mainStore, optionsStore")
@observer
export default class App extends React.Component {
constructor(props) {
super(props);
};

render() {
<div>
{this.props.optionsStore.appName}
</div>
}
}

Здесь присутствуют две важных сущности Mobx — @inject и @observer.

@inject внедряет только то хранилище (из представленных на верхнем уровне через Provider), которое будет нужно непосредственно в этом компоненте. Разные части нашего приложения используют разные хранилища, которые мы перечисляем в inject через запятую. Хранилища доступны в компоненте через this.props.yourStoreName.

@observer производит подписку на изменение данных в хранилищах. Сам механизм подписки скрыт в библиотеке Mobx, мы лишь декларируем, что хотим знать о том, что данные в этих хранилищах изменились. Таким образом, мы избавились от подписок на изменение событий, как это требовалось бы в чистом JS, и от пробрасывания колбэков в родительские компоненты, как если бы мы использовали чистый React. Теперь Mobx отвечает за доставку всех изменений данных прямо в компоненты!

События и реакции

Перейдем к хранилищу одного из компонентов и посмотрим, как обрабатываются пользовательские события:

import {action, autorun, observable} from 'mobx';
import optionsStore from "./optionsStore";
import {
getTarget,
sendStats
} from "../common/heplers";

export default class EmailStore {
constructor() {
autorun(() => {
sendStats();
});
}

@observable params = {
value : "",
disabled : null,
isCorrect : null,
isWrong : null,
onceValidated : false
};

@action bindUserData = (e) => {
if (e) e.preventDefault();
this.params.value = getTarget(e).value;
this.validate(this.params.value);
};

@action validate = (data) => {
if (data && data.match(optionsStore.emailRegexp)) {
this.params.isCorrect = true;
this.params.onceValidated = true;
this.params.isWrong = false;
} else {
this.params.isCorrect = false;
if (this.params.onceValidated) this.params.isWrong = true;
}
};
}

Еще несколько важных сущностей Mobx:

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

@observable — объект, за изменением полей которого следит Mobx. Если хотя бы одно из полей объекта изменилось, Mobx доставляет его новое значение компоненту, который мы обернули декораторами @observer и @inject (с указанием именно этого хранилища).

@action — специальный декоратор для обертывания хендлеров любых событий, которые должны поменять state приложения и/или вызвать сайд-эффекты. В примере выше пользователь вводит значение email, которое мы записываем в поле value observable-объекта params (первое изменение state), а потом валидируем и меняем значения других полей в params.

В коде UI компонента @action вызываются так же, как и обычные хендлеры в React:

render() {
return (
<div className="email-input">
<label htmlFor="email">Please type yor email</label>
<input
type="email"
disabled={this.props.disabled}
name="email"
id="email"
value={EmailStore.params.value}
onChange={(e) => EmailStore.bindUserData(e)}
/>
</div>
);
}

Реакции могут быть не только на пользовательские события, но и на изменение данных:

import {action, computed, get, observable, reaction} from 'mobx';
import optionsStore from "./optionsStore";
import emailStore from "./emailStore";

export default class Reactions {
constructor(props) {
reaction(
() => this.emailAction,
(result) => {
this.userData.emailValue = result.value;
this.userData.emailIsCorrect = result.isCorrect;
if (result.onceValidated) this.makeFormValidated();
}
);
}

@observable userData = {
emailValue : null,
emailIsCorrect : false
}

@action makeFormValidated = () => {
// do some side-effect..
}

@computed get emailAction() {
const p = EmailStore.params;
return {
value : p.value,
isCorrect : p.isCorrect,
onceValidated : p.onceValidated
}
};
}

@computed — декоратор для функций, которые отслеживают изменения в observable-объектах. Важным преимуществом Mobx является то, что отслеживаются только данные, которые вычисляются непосредственно в этой функции и потом возвращаются в качестве результата. То есть, если в EmailStore.params три перечисленных в блоке @computed return параметра не менялись (value, isCorrect, onceValidated), то @computed не будет производить никаких вычислений. Как видно на этом примере, observable-объекты для @computed могут браться из любого места приложения, в том числе из другого хранилища данных.

reaction — инструмент для организации сайд-эффектов на основе изменившегося состояния. Он принимает две функции: первая computed, возвращающая новое вычисленное состояние, вторая — функция с эффектами, которые должны последовать вслед за изменением состояния.

Информация о состоянии в Mobx доставляется мгновенно

Одним из серьезных преимуществ библиотеки Mobx является то, что состояние в ней консистентно.

Мы помним, что изначально в React изменение состояния this.setState() представляет собой асинхронный вызов. То есть мы не можем точно сказать, когда состояние действительно изменится, мы лишь ставим запрос на изменение состояния в общую очередь.

Mobx, напротив, гарантирует, что состояние изменится ровно в тот момент, когда будет дана команда в коде. Это означает, что буквально в следующей строке мы можем использовать уже новое состояние, записанное в объекте @observable:

@action bindUserData = (e) => {
this.params.value = getTarget(e).value;
this.validate(this.params.value);
};

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

C Mobx + React колбэков больше нет

Поскольку заботу об изменении состояния и доставке его до потребителя (то есть компонента) берёт на себя сама библиотека, нам больше не нужны функции-колбэки, передаваемые через props от родительских компонентов к дочерним, как мы это делали в обычном React.

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

Мы также можем вынести набор часто используемых методов в отдельное хранилище — назовём его Actions — которое может обрабатывать действия и выдавать сайд-эффекты (менять состояние). В некотором смысле у нас теперь единое хранилище для колбэков, которые больше не привязаны к конкретным компонентам в иерархии приложения. Это также позволяет упростить поддержку кода и избавиться от дублирования функций.

Возможная модель хранилища данных в Mobx

Поскольку Mobx позволяет не привязывать модель данных к иерархии компонентов UI, мы можем заняться построением удобного хранилища данных сами. Mobx не навязывает одну модель построения хранилища данных, и оставляет этот вопрос на выбор разработчика. Это означает, что модель хранилища может иметь собственную иерархию, либо быть плоской.

На практике с плоской моделью данных взаимодействовать трудно, так как нужно согласование взаимодействия компонентов хотя бы на уровне приложения. Конечно, можно писать связи many-to-many. Но тогда в каждом из хранилищ придется настраивать свои computed и reactions, которые будут ждать изменений в других хранилищах. Также встает вопрос порядка инициализации хранилищ.

Поэтому минимально разумной представляется одноступенчатая иерархия вложенности хранилищ данных. Создается по одному изолированному хранилищу на каждый обособленный компонент или тип компонентов.

Например, у нас на странице 10 текстовых полей ввода одного типа, на всех один store с названием InputStore. Оркестрированием работы всего приложения занимается mainStore — хранилище, которое знает обо всех других хранилищах.

В начале статьи приведён пример подключения хранилищ через Provider. Видно, что хранилища компонентов представлены как свойства главного хранилища:

const stores = {
mainStore,
optionsStore,
ButtonStore : mainStore.ButtonStore,
FioStore : mainStore.FioStore,
EmailStore : mainStore.EmailStore
};

Кроме того, мы можем подключить статическое хранилище словарей (в примере выше optionsStore), которое будет отдавать заранее известный набор данных и не меняться в процессе работы.

При такой схеме работы мы имеем следующее направление потока данных: компонентные хранилища ничего не знают о других хранилищах, в том числе mainStore, и не вызывают никаких сторонних эффектов. Единственное, что положено компонентному хранилищу — это изменять свое состояние. В качестве исключения такое хранилище может брать данные из хранилища-словаря (в нашем случае optionsStore).

Соответственно, mainStore знает все о других хранилищах (оно инициализирует их в своем конструкторе), а также слушает через @computed все изменения состояний, необходимых для работы.

При таком подходе mainStore со временем может сильно разрастись, поэтому имеет смысл также разделить его на три составных части: собственно mainStore, где будет происходить первоначальная инициализация, а также содержатся все @obvervable и @computed.

В отдельную часть можно вынести «библиотеку колбэков» Actions и «библиотеку хендлеров» Reactions:

// mainStore.js
import {computed, get, observable} from 'mobx';
import optionsStore from "./optionsStore";
import ButtonStore from "./ButtonStore";
import FioStore from "./FioStore";
import EmailStore from "./EmailStore";
import Actions from "./Actions";
import Reactions from "./Reactions";

class mainStore {
constructor() {
this.ButtonStore = new ButtonStore();
this.FioStore = new FioStore();
this.EmailStore = new EmailStore();
this.Actions = new Actions(this);
this.Reactions = new Reactions(this);
}

@observable userData = {
name : "",
surname : "",
email : ""
};

@observable buttons = {
sendData : {
disabled : true
}
};


@computed get emailAction() {
const p = this.EmailStore.params;
return {
value : p.value,
isCorrect : p.isCorrect,
onceValidated : p.onceValidated
}
};
}

// Actions.js
import {action, get} from 'mobx';
export default class Actions{
constructor(props) {
this.props = props;
this.ButtonStore = props.ButtonStore;
this.FioStore = props.FioStore;
this.EmailStore = props.EmailStore;
this.fillBlocks();
};

@action fillBlocks = () => {
// do something
};

@action hideElement = (el) => {
// do something
}
}

// Reactions.js
import {reaction, get} from 'mobx';
export default class Reactions{
constructor(props) {
this.props = props;
this.ButtonStore = props.ButtonStore;
this.FioStore = props.FioStore;
this.EmailStore = props.EmailStore;

reaction(
() => props.emailAction,
(result) => {
props.userData.emailValue = result.value;
props.userData.emailIsCorrect = result.isCorrect;
if (result.onceValidated) props.Actions.makeFormValidated();
}
);
};
}

Общая схема хранилищ данных выглядит следующим образом:

-3

Что дает применение React в связке с Mobx

  • Мы избегаем неконсистентности состояния приложения. Бизнес-логика не размазывается по иерархии компонентов.
  • Не нужно писать больше колбэков для передачи на верхний уровень информации о состоянии компонентов.
  • Компоненты приложения проще переиспользовать.

Автор: Максим Слепов

Источник: https://mcs.mail.ru/blog/plyusy-svyazki-mobx-react-dlya-upravleniya-sostoyaniem-prilozheniya

Что еще почитать:
Как не превратиться в бомжа, если ты разработчик
Разработка приложений в VK mini apps: полная инструкция
Полезные и простые инструменты командной строки