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 пока не участвует.


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 приложении.
image


Вывод


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


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

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


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


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

Еще


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


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


  1. vintage
    02.05.2016 11:58
    +2

    Круто, не правда ли? report вызывается автоматически, синхронно, без утечки промежуточных значений.

    Не очень круто. Обычно всё-таки промежуточные состояния не имеют никакого интереса и синхронное обновление состояний по всем промежуточным значениям приводит лишь к лишним тормозам.


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

    В какой момент она перестанет это делать?


    1. JiLiZART
      02.05.2016 12:06

      Если нужно, чтобы отслеживание функции прекратилось в какой то момент, для этого есть функция when https://mobxjs.github.io/mobx/refguide/when.html


      1. vintage
        02.05.2016 15:49
        +1

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


  1. Klimashkin
    03.05.2016 11:53
    +2

    Knockout.js + ES6 + React(как view) = MobX

    Забавно получается, React + Flux(как концепция) был предложен именно как альтернатива MVC/MVVM, чтобы покончить наконец с адом кроссзависимостей данных, которое всегда происходит при использовании паттерна observable.
    Если здесь еще есть front-end разработчики с опытом больше хотя бы трех лет, они вспомнят какого это — когда observable стоит на observable и observable'ом погоняет, и над ними начинают появляться computed c некоторым throttle, чтобы хоть немного развязать во времени клубок вызывающих рендер обновлений данных.

    Flux-подход был решением, путем введения unidirectional data flow которым можно полностью управлять и вызывать render через setState когда нужно. Некоторые flux-библиотеки испортили этот подход, скрывая от программиста некоторые возможности управления рендером, а react-router еще и подлил масла в огонь, конкурируя за управления стэйтом с flux.

    В итоге программист, желая быстро что-то написать, все равно потерял контроль над управлением рендером и опять появился observable, чтобы «упростить» его жизнь. На время, потому что с ростом сложности проекта, опять наступить ад кроссзависимостей, опять обновления начнут разлетаться во все стороны и прилетать с разных сторон.

    MobX хорош для старта и небольших проектов. Не переоценивайте его.


    1. ikido
      07.05.2016 19:46

      Flux-подход был решением, путем введения unidirectional data flow которым можно полностью управлять и вызывать render через setState когда нужно. Некоторые flux-библиотеки испортили этот подход

      Некоторые? Сама концепция React+Flux строится на том что мы на каждое изменение стора (неважно один он у вас или несколько) делаем render, потому что он «бесплатный» и у нас есть Virtual DOM, и только в исключительных случаях используем shouldComponentUpdate. Mobx (и mobx-react) как раз позволяет использовать sideways data loading, когда у вас ре-рендер происходит не на каждый чих стора, а при изменении конкретных данных, которые использует компонент


      1. Klimashkin
        07.05.2016 22:29

        > Сама концепция React+Flux строится на том что мы на каждое изменение стора (неважно один он у вас или несколько) делаем render, потому что он «бесплатный»

        Это в приложениях уровня TODO, а в реальных проектах с сотнями компонент и десятками сторов так никто не делает, иначе отладка становится невыносимой и рендер/сравнение виртуального дома становится дороже чем обновить реальный. Чтобы этого небыло, применяют техники, когда ждут выполнения всех асинхронных запросов в экшене и или тому подобное (можно думать об этом как о транзакциях) — это возможно и в redux и в relay


        1. ikido
          08.05.2016 02:24

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


        1. ikido
          08.05.2016 02:31

          Да даже если и ждут, классика Flux — пришел ответ с сервера, у нас данные нормализованы и лежат по своим сторам. Мы подождали, пускай и несколько, асинхронных запросов и сразу 10 сторов эмитят событие 'change'. Начинаются каскадные ре-рендеры, которые надо вручную обходить через shouldComponentUpdate (см ниже)

          У redux другая болезнь, в реальном, как вы говорите, приложении, у вас либо мало компонентов подписаны на единственный стор и тогда у вас боль с пробрасыванием данных через props или context, либо подписано много компонентов и тогда на каждый чих стора у вас… тадам — ре-рендеры, которые надо вручную обходить через shouldComponentUpdate =)

          с shouldComponentUpdate отдельная песня, учитывая что данные обычно нормализуют их потом приходится денормализовывать, а js, гад, часто выдает как результат этого всегда новые массивы и обьекты.

          Вот чтобы избавить от этой боли и был создан mobx, и это не имеет ничего общего с ужасом типа Knockout, имхо.


  1. AndreyRubankov
    03.05.2016 11:54

    observableTodoStore.addTodo(«try MobX»);
    observableTodoStore.todos[0].completed = true;

    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 });

    странный подход — то работать через апи, то прямо в кишки лезть…
    а ведь если работать через апи класса, то неконсистентного состояния и быть не могло бы =)

    В целом MobX привносит «магию» для замены простой концепции event dispatching (или pub/sub, если так приятнее).
    Более того, если в апи добавить метод change, который будет изменять название и описание задания то, согласно коду выше, это породит 2 события изменения и изменение DOM произойдет 2 раза.
    И это на примере кода уровня «hello world».


    1. ikido
      08.05.2016 02:37

      В целом да, но это так же как сказать что React привносить магию в концепцию шаблонирования, а например, node.js в концепцию CGI =) Про метод change — если вы начинаете додумывать и программировать в уме то делайте рефакторинг там же и подписывайтесь на конкретный todo, а не на его название описание и название, тогда будет одно событие. У автора же цель показать как это работает, а не додумать за вас все возможные добавления методов в API


      1. AndreyRubankov
        08.05.2016 06:30

        на счет того, что статьи описывают идеи я понимаю, но ведь описано не очень хорошо, как писал раньше: то через апи, то через кишки (хотя, судя по тому, как пишут код в доках mobX — это их путь).

        но хорошо, пойдем по пути документации: https://mobxjs.github.io/mobx/refguide/observable.html
        аннотация/декоратор вешается только на массив, логично ждать срабатывания только, если изменится состояние структуры массива (добавили, удалили, заменили элементы).
        но события публикуются и при изменении данных, которые лежат в массиве.
        а соответственно, если необходимо изменить 2+ поля в данных — это породит 2+ событий, обойти это можно только если никогда не изменять данные, которые уже лежат в массиве: пересоздавать + перезаписывать данные.

        > если вы начинаете додумывать и программировать в уме то делайте рефакторинг там же и подписывайтесь на конкретный todo
        на конкретный туду — не исправит проблемы, зато кода будет в разы больше и он будет в разы хуже.
        ps: а разве программировать можно как-то иначе, кроме как в уме? херяк-херяк, авось заработает — это плохой подход.


        1. ikido
          08.05.2016 09:17

          Вы совершаете ту же ошибку что и автор — бегло смотрите доки) Ну или в доках этот момент явно не прописан. Есть модификатор asFlat, который позволяет подписываться только на изменение массива — добавление/удаление элементов, при этом не слушая их «дочерние» изменения.

          Про программирование в уме — я прошу прощения если это прозвучало грубо, просто автор привел конкретный пример, можно долго фантазировать «а если бы» но это уже будет другой пример =) Из серии «а если поставить там-то точку с запятой то будет SyntaxError».


          1. AndreyRubankov
            08.05.2016 11:48

            Спасибо! Да, этого я не досмотрел. С такой возможностью действительно все будет отлично.


  1. ikido
    08.05.2016 11:53

    Возможно по такому небольшому обзору это не очень понятно, но mobx/mobx-react действительно очень полезная вещь, и она набирает популярность. Если вам знакома боль от кучи экшнов/редьюсеров redux-а и большого количества boilerplate кода в компонентах то посмотрите на mobx. Мы его начали использовать в крупном проекте пол года назад и не только уменьшили количество кода в целом, но и сделали его более доступным для понимания новых разработчиков разного уровня. Готов поделиться подробностями, если кому интересно, без смс и регистрации =)


    1. vintage
      08.05.2016 12:37

      Конечно интересно, рассказывайте.


      1. ikido
        08.05.2016 12:38

        А давайте вы спросите что-то конкретное? А то прям статью написать у меня к сожалению пока не получается, хотя и очень хочется.


        1. vintage
          08.05.2016 13:02

          Конкретно — хочу статью с примерами :-) Я ж не знаю как вы там всё обустроили.


          1. ikido
            08.05.2016 13:04

            Это вы конечно хитро придумали! Смысл не в том как мы все обустроили, а как какую-то вашу проблему можно решить с помощью этой библиотеки. С другой стороны если у вас проблем нет — то может оно вам и не надо)


            1. vintage
              08.05.2016 13:07

              Чтобы увидеть проблему — нужно увидеть её отсутствие. Покажите пример, не будьте голословными :-)


    1. JiLiZART
      11.05.2016 14:04

      Пишите статью "Как мы перешли на mobx и избавились от боли".
      А там приведите, примеры, как у вас устроено приложение сейчас.
      Почему так устроено.
      Как было до этого.
      Какую проблему вы пытались решить этим переходом.


      1. ikido
        11.05.2016 15:11

        Спасибо за руководство к действию, но к сожалению сейчас нет времени писать статью. Отдельное вам спасибо что вы это время нашли! Если есть какие-то конкретные вопросы то я постараюсь ответить.