Mobx — управление состоянием вашего приложения

в 8:19, , рубрики: FRP, mobx, React, reactive, ReactJS, redux, store

MobX это простое, опробованное в бою решение для управления состоянием вашего приложения. Этот туториал научит вас основным концептам MobX. MobX это автономная библиотека, но большинство используют ее в связке с React и этот туториал будет сфокусирован на этой комбинации.

Основная идея

Состояние (state ориг.) это сердце каждого приложения и нет более быстрого способа создания забагованого, неуправляемого приложения, как отсутствие консистентности состояния. Или состояние, которое несогласованно с локальными переменными вокруг. Поэтому множество решений по управлению состоянием пытаются ограничить способы, которыми можно его изменять, например сделать состояние неизменяемым. Но это порождает новые проблемы, данные нуждаются в нормализации, нет гарантии ссылочной целостности и становится почти невозможно использовать такие мощные концепты как прототипы(prototypes ориг.).

MobX позволяет сделать управление состоянием вновь простым, вернувшись к корню проблемы: он делает невозможным инконсистентность состояния. Стратегия достижения этого довольно проста: убедится что, все что может быть вынуто из состояния, будет вынуто. Автоматически.

Концептуально MobX обрабатывает ваше приложение как электронная таблица (отсылка к офисной программе для работы с таблицами прим. пер.).

MobX Cycle

  • Во-первых, есть состояние State приложения. Графы объектов, массивов, примитивов, ссылок которые формируют модель вашего приложения.

  • Во-вторых есть произовдные Derivations. Обычно, это любое значение, которое может быть вычислено автоматически из данных состояния вашего приложения.

  • Реакции Reactions очень похожи на производные Derivations. Основное отличие: они не возвращают значение, но запускаются автоматически, чтобы выполнить какую то работу. Обычно это связано с I/O. Они проверяют, что DOM обновился или сетевые запросы выполнились вовремя

  • Наконец, есть действия Actions. Действия это все те штуки которые меняют состояние. MobX проследит, чтобы все изменения в состоянии приложения, вызванные действиями, автоматически обработались всеми производными и реакциями. Синхронно и без помех.

Простой todo store...

Довольно теории, рассмотрим его в действии, будет намного понятнее, чем внимательно читать написанное выше. Ради оригинальности давайте начнем с очень простого Todo хранилища. Ниже приведен очень простой TodoStore, который управляет коллекцией todo. MobX пока не участвует.

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({ 
            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 декораторы.

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({ 
            task: task,
            completed: false,
            assignee: null
        });
    }
}

const observableTodoStore = new ObservableTodoStore();

That's it! We marked some properties as being @observable to signal MobX that these values can change over time. The computations are decorated with @computed to identify that these can be derived from the state.
Вот и все! Мы пометили некоторые свойства как @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={ idx } />
        ) }
        </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')); 
  } 
}

@observer
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 автоматически вычислит и обновит соответствующие части вашего пользовательского интерфейса из состояния в вашем хранилище.```javascript
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...

<h4>Работа со ссылками</h4>
До сих пор мы создавали отслеживаемые объекты (с прототипом и без), массивы и примитивы. Но вам может показаться интересным, как обрабатываются ссылки в MobX? В предыдущих листингах, вы могли заметить `assignee` свойство у `todos`. Давайте дадим ему некоторое другое значение, создав еще одно хранилище *(ладно, это просто массив)* содержащее людей, и назначим их к задачам.```javascript
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 приложении.
image

Вывод

На этом все! Никакого бойлерплейта. Простые и декларативные компоненты которые формируют UI легко и просто. Полностью обновляются из состояния. Теперь вы готовы начать использовать пакеты mobx и mobx-react в вашем приложении.
Краткое резюме вещей которые вы сегодня узнали:

  1. Используйте ` @observable` декоратор или `observable(объект или массив)` функцию чтобы сделать ваши объекты отслежываемыми MobX
  2. Декоратор ` @computed` может быть использован для создания функций которые вычисляют свое значение из состояния
  3. Используйте `autorun`, чтобы автоматически запускать ваши функции на основе отслеживаемого состояния. Это применимо для логирования или сетевых запросов.
  4. Используйте декоратор `@observer` из пакета `mobx-react`, чтобы наделить ваши React компоненты реактивной силой. Они автоматически будут наиболее эффективно обновляться. Даже в больших и сложных приложениях с большим количеством данных.

MobX не контейнер состояния

Люди часто используют MobX как альтернативу Redux. Но пожалуйста, обратите внимание, что это просто библиотека, для решения определенной проблемы а не архитектура или контейнер состояния. В этом смысле приведенные выше примеры являются надуманными и рекомендуется использовать правильные архитектурные решения, как инкапсуляция логики в методах, их организация в хранилищах или контроллерах и т.д. Или как кто то написал на Hacker News:

«Использовать MobX означает использование контроллеров, диспетчеров, действий, супервизоров или любой другой формы управления потоком данных, это ведет нас к тому, что архитектурную потребность вашего приложения проектируете вы сами, а не используя то что используют по умолчанию для чего то большего чем Todo приложение»

Еще

Заинтригованы? Вот некоторые полезные ссылки (на английском прим. пер.):

  1. [MobX на GitHub](https://github.com/mobxjs/mobx)
  2. [Api документация](https://mobxjs.github.io/mobx)
  3. (Блогозапись) [Making React reactive: the pursuit of high performing, easily maintainable React apps](https://www.mendix.com/tech-blog/making-react-reactive-pursuit-high-performing-easily-maintainable-react-apps/)
  4. (Блогозапись) [Becoming fully reactive: an in-depth explanation of Mobservable](https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254#.4kvwpd2nh)
  5. [JSFiddle](https://jsfiddle.net/mweststrate/wv3yopo0/) c простым todo приложением
  6. [MobX, React, TypeScript boilerplate](https://github.com/mobxjs/mobx-react-typescript-boilerplate)
  7. [MobX, React, Babel boilerplate](https://github.com/mobxjs/mobx-react-boilerplate)
  8. [MobX демо с конференции Reactive2015](https://github.com/mobxjs/mobx-reactive2015-demo)
  9. [MobX + React TodoMVC](https://github.com/mobxjs/mobx-react-todomvc)

Автор: JiLiZART

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js