В «настоящих» проектах мы получаем данные от сервера или пользовательского ввода, форматируем, валидируем, нормализуем и производим другие операции над ними. Всё это принято считать бизнес логикой и должно быть помещено в Model. Так как react — это только треть MVC пирога, для создания пользовательских интерфейсов, то нам потребуется еще что-то для бизнес логики. Некоторые используют паттерны redux или flux, некоторые — backbone.js или даже angular, мы же будем использовать mobx.js в качестве Model.

В предыдущей статье мы уже подготовили фундамент, будем строить на нём. Так как mobx — это standalone библиотека, то для связки с react-ом нам понадобится mobx-react:

npm i --save mobx mobx-react

Кроме того, для работы с декораторами и трансформации свойств классов нам потребуются babel плагины babel-plugin-transform-class-properties и babel-plugin-transform-decorators-legacy:

npm i --save-dev babel-plugin-transform-decorators-legacy babel-plugin-transform-class-properties

Не забудем добавить их в .babelrc

  "plugins": [
    "react-hot-loader/babel",
    "transform-decorators-legacy",
    "transform-class-properties"
  ]

У нас есть компонента Menu, давайте продолжим работу с ней. У панели будет два состояния «открыта/закрыта», а управлять состоянием будем с помощью mobx.

1. Первым делом нам нужно определить состояние и сделать его наблюдаемым посредством добавления декоратора @observable. Состояние может быть представлено любой структурой данных: объектами, массивами, классами и прочими. Создадим хранилище для меню (menu-store.js) в директории stores.

import { observable} from 'mobx';

class MenuStore {
  @observable show;

  constructor() {
    this.show = false;
  }
}

export default new MenuStore();

Стор представляет собой ES6 class с единственным свойством show. Мы повесили на него декоратор @observable, тем самым сказали mobx-у наблюдать за ним. Show — это состояние нашей панели, которое мы будем менять.

2. Создать представление, реагирующее на изменение состояния. Хорошо, что у нас уже оно есть, это component/menu/index.js. Теперь, когда состояние будет изменяться, наше меню будет автоматически перересовываться, при этом mobx найдет кротчайший путь для обновления представления. Что бы это произошло, нужно обернуть функцию, описывающую react компонент, в observer.

components/menu/index.js

import React from 'react';
import cn from 'classnames';
import { observer } from 'mobx-react';

/* stores */
import menuStore from '../../stores/menu-store';

/* styles */
import styles from './style.css';

const Menu = observer(() => (
  <nav className={cn(styles.menu, { [styles.active]: menuStore.show })}>
    <div className={styles['toggle-btn']}>☰</div>
  </nav>
));

export default Menu;


В любом react приложении нам понадобится утилита classnames для работы с className. Раньше она входила в пакет react-а, но теперь ставится отдельно:

npm i --save classnames

c её помощью можно склеивать имена классов, используя различные условия, незаменимая вещь.
Видно, что мы добавляем класс «active», если значение состояние меню show === true. Если в конструкторе хранилища поменять состояние на this.show = true, то у панели появится «active» класс.

3. Осталось изменить состояние. Добавим событие click для «гамбургера» в
menu/index.js
<div
      onClick={() => { menuStore.toggleLeftPanel() }}
      className={styles['toggle-btn']}>☰</div>

и метод toggleLeftPanel() в
stores/menu-store.js
import { observable } from 'mobx';

class MenuStore {
  @observable show;

  constructor() {
    this.show = false;
  }

  toggleLeftPanel() {
    this.show = !this.show;
  }
}

const menuStore = new MenuStore();

export default menuStore;
export { MenuStore };


Note: По дефолту мы экспортируем хранилище как инстанс синглтона, также экспортируется и класс напрямую, так как он тоже может понадобиться, например, для тестов.

Для наглядности добавим стили:

components/menu/styles.css
.menu {
  position: fixed;
  top: 0;
  left: -180px;
  bottom: 0;
  width: 220px;
  background-color: tomato;
  &.active {
    left: 0;
  }
  & .toggle-btn {
    position: absolute;
    top: 5px;
    right: 10px;
    font-size: 26px;
    font-weight: 500;
    color: white;
    cursor: pointer;
  }
}


И проверим, что по клику на иконку, наша панель открывается и закрывается. Мы написали минимальный mobx store для управления состоянием панели. Давайте немного нарастим мяса и попробуем управлять панелью из другого компонента. Нам потребуются дополнительные методы для открытия и закрытия панели:

stores/menu-store.js
import { observable, computed, action } from 'mobx';

class MenuStore {
  @observable show;

  constructor() {
    this.show = false;
  }

  @computed get isOpenLeftPanel() {
    return this.show;
  }

  @action('toggle left panel')
  toggleLeftPanel() {
    this.show = !this.show;
  }

  @action('show left panel')
  openLeftPanel() {
    this.show = true;
  }

  @action('hide left panel')
  closeLeftPanel() {
    this.show = false;
  }
}

const menuStore = new MenuStore();

export default menuStore;
export { MenuStore };


Можно заметить, что мы добавили computed и action декораторы, они обязательны только в strict mode (по умолчанию отключено). Computed значения будут автоматически пересчитаны при изменении соответствующих данных. Рекомендуется использовать action, это поможет лучше структурировать приложение и оптимизировать производительность. Как видно, первым аргументом мы задаём расширенное название производимого действия. Теперь при деббаге мы сможем наблюдать, какой метод был вызван и как менялось состояние.



Note: При разработке удобно использовать расширения хрома для mobx и react, а так же react-mobx devtools

Создадим еще один компонент
components/left-panel-controller/index.js
import React from 'react';

/* stores */
import menuStore from '../../stores/menu-store';

/* styles */
import styles from './styles.css';

const Component = () => (
  <div className={styles.container}>
    <button onClick={()=>{ menuStore.openLeftPanel(); }}>Open left panel</button>
    <button onClick={()=>{ menuStore.closeLeftPanel(); }}>Close left panel</button>
  </div>
);

export default Component;


Внутри пара кнопок, которые будут открывать и закрывать панель. Этот компонент добавим на Home страницу. Должно получиться следующее:

структура


В браузере это будет выглядеть так:

mobx в работе


Теперь мы можем управлять состоянием панели не только из самой панели, но и из другого компонента.
Note: если несколько раз произвести одно и тоже действие, например, нажать кнопку «close left panel», то в деббагере можно видеть, что экшен срабатывает, но никакой реакции не происходит. Это значит, что mobx не перересовывает компонент, так как состояние не изменилось и нам не нужно писать «лишний» код, как для pure react компонент.

Осталось немного причесать наш подход, работать со сторами приятно, но разбрасывать импорты хранилищ по всему проекту некрасиво. В mobx-react для таких целей появился Provider (см. Provider and inject) — компонент, который позволяет передавать сторы (и не только) потомкам, используя react context. Для этого обернем корневой компонент app.js в Provider:

app.js
import React from 'react';
import { Provider } from 'mobx-react';
import { useStrict } from 'mobx';

/* components */
import Menu from '../components/menu';

/* stores */
import leftMenuStore from '../stores/menu-store';

/* styles */
import './global.css';
import style from './app.css';

useStrict(true);

const stores = { leftMenuStore };

const App = props => (
  <Provider { ...stores }>
    <div className={style['app-container']}>
      <Menu />
      <div className={style['page-container']}>
        {props.children}
      </div>
    </div>
  </Provider>
);

export default App;


Тут же импортируем все сторы (у нас один) и передаём их провайдеру через props. Так как провайдер работает с контекстом, то сторы будут доступны в любом дочернем компоненте. Также разобьем menu.js компонент на два, чтобы получился «глупый» и «умный» компонент.

components/menu/menu.js
import React from 'react';
import cn from 'classnames';

import styles from './style.css';

const Menu = props => (
  <nav className={cn(styles.menu, { [styles.active]: props.isOpenLeftPanel })}>
    <div onClick={props.toggleMenu}
         className={styles['toggle-btn']}>☰</div>
  </nav>
);

export default Menu;


components/menu/index.js
import React from 'react';
import { observer, inject } from 'mobx-react';

import Menu from './menu'

const Component = inject('leftMenuStore')(observer(({ leftMenuStore }) => (
  <Menu
    toggleMenu={() => leftMenuStore.toggleLeftPanel()}
    isOpenLeftPanel={leftMenuStore.isOpenLeftPanel} />
)));

Component.displayName = "MenuContainer";
export default Component;


«Глупый» нам не интересен, так как это обычный stateless компонент, который получает через props данные о том открыта или закрыта панель и колбэк для переключения.

Гораздо интереснее посмотреть на его враппер: мы видим тут HOC, где мы инжектим необходимые сторы, в нашем случае «leftMenuStore», в качестве компонента мы передаем наш «глупый компонент», обернутый в observer. Так как мы приинжектили leftMenuStore, то хранилище теперь доступно через props.

практически тоже самое мы проделываем с left-panel-controller:

components/left-menu-controller/left-menu-controller.js
import React from 'react';

/* styles */
import style from './styles.css';

const LeftPanelController = props => (
  <div className={style.container}>
    <button onClick={() => props.openPanel()}>Open left panel</button>
    <button onClick={() => props.closePanel()}>Close left panel</button>
  </div>
);

export default LeftPanelController;


components/left-menu-controller/index.js
import React from 'react';
import { inject } from 'mobx-react';

import LeftPanelController from './left-panel-controller';

const Component = inject('leftMenuStore')(({ leftMenuStore }) => {
  return (
    <LeftPanelController
      openPanel={() => leftMenuStore.openLeftPanel()}
      closePanel={() => leftMenuStore.closeLeftPanel()} />
  );
});

LeftPanelController.displayName = 'LeftPanelControllerContainer';
export default Component;


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

Note: я использую displayName для задания имени компоненту, это удобно для деббага:

Например, теперь можно найти компонент через поиск


Это все просто, теперь давайте получим данные с сервера, пусть это будет список пользователей с чекбоксами.

Идем на сервер и добавляем роут "/users" для получения пользователей:

server.js
const USERS = [
  { id: 1, name: "Alexey", age: 30 },
  { id: 2, name: "Ignat", age: 15 },
  { id: 3, name: "Sergey", age: 26 },
];
...
app.get("/users", function(req, res) {
  setTimeout(() => {
    res.send(USERS);
  }, 1000);
});


Нарочно добавим задержку, чтобы проверить, что приложение работает корректно даже с большим интервалом ответа сервера.

Далее нам понадобится

user-store:
import { observable, computed, action, asMap, autorun } from 'mobx';

class User {
  @observable user = observable.map();

  constructor(userData = {}, checked = false) {
    this.user.merge(userData);
    this.user.set("checked", checked);
  }

  @computed get userInfo() {
    return `${this.user.get("name")} - ${this.user.get("age")}`;
  }

  @action toggle() {
    this.user.set("checked", !this.user.get("checked"));
  }
}

class UserStore {
  @observable users;

  constructor() {
    this.users = [];
    this.fetch();
  }

  @computed get selectedCount() {
    return this.users.filter(userStore => {
      return userStore.user.get("checked");
    }).length;
  }

  getUsers() {
    return this.users;
  }

  @action fetch() {
    fetch('/users', { method: 'GET' })
      .then(res => res.json())
      .then(json => this.putUsers(json));
  }

  @action putUsers(users) {
    let userArray = [];
    users.forEach(user => {
      userArray.push(new User(user));
    });
    this.users = userArray;
  }
}

const userStore = new UserStore();

autorun(() => {
  console.log(userStore.getUsers().toJS());
});

export default userStore;
export { UserStore };


Тут описан класс User со свойством user. В mobx есть observable.map тип данных, он как раз подойдет нам для описания user-а. Грубо говоря, мы получаем наблюдаемый объект, причем, наблюдать можно за изменением конкретного поля. Также становятся доступны getter, setter и прочие вспомогательные методы. Например, в конструкторе с помощью «merge», мы легко можем скопировать поля из userData в user. Это очень удобно, если объект содержит много полей. Также напишем один action для переключения состояния пользователя и вычисляемое значения для получения информации о пользователе.

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

Note: autorun выполняет функцию автоматически, если наблюдаемое значение было изменено. Для примера, тут выводится все пользователи в консоль. Если попробовать достать пользователей методом «getUsers()», то можно заметить, что тип возвращаемых данных не Array, а ObservableArray. Для конвертации observable объектов в javascript структуру, используем toJS().

В app.js не забудем дописать новый user-store, чтобы потомки могли им пользоваться.

Добавим react компоненты в директорию components:

user-list/index.js
import React from 'react';
import { observer, inject } from 'mobx-react';

import UserList from './user-list';

const Component = inject('userStore')(observer(({ userStore }) => {
  return (
    <UserList
      users={userStore.getUsers()}
      selectedUsersCount={userStore.selectedCount} />
  );
}));

Component.displayName = 'UserList';
export default Component;


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

user-list/user-list.js
import React from 'react';

/* components */
import UserListItem from './user-list-item';

/* styles */
import style from './styles.css';

const UserList = props => {
  return (
    <div className={style.container}>
      <ul>
        {props.users.map(userStore => {
          return (
            <UserListItem
              key={userStore.user.get('id')}
              isChecked={userStore.user.get('checked')}
              text={userStore.userInfo}
              onToggle={() => userStore.toggle()} />);
        })}
      </ul>
      <span>{`Users:${props.users.length}`}</span>
      <span>{`Selected users: ${props.selectedUsersCount}`}</span>
    </div>
  );
};

export default UserList;


Показываем список пользователей и информацию по их количеству. Передаём «toggle()» метод стора через props.

user-list/user-list-item.js
import React from 'react';

const UserListItem = props => (
  <li><input type="checkbox" checked={props.isChecked} onClick={() => props.onToggle()} />{props.text}
  </li>
);
export default UserListItem;


Рендерим одного пользователя.

Добавляем стили и цепляем готовый компонент на Home страницу. Все готово(github), можно поиграть с чекбоксами и убедиться, что все методы работают.

В итоге мы увидели как работает mobx в связке с react-ом, учитывая все возможности mobx, можно предположить, что такое решение имеет право на жизнь. Mobx прекрасно справляется с обязанностью менеджера состояний для react приложений и предоставляет богатый функционал для реализации.
Поделиться с друзьями
-->

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


  1. comerc
    20.03.2017 16:54
    -1

    Как обычно, хочется услышать несколько слов, почему MobX а не Redux. Для непосвященных.


    1. VolCh
      20.03.2017 17:17
      +4

      Вообще туториалы типа "как настроить с нуля такую-то связку" не предполагают раскрытия вопроса "почему эту связку, а не конкурирующую".


      1. comerc
        20.03.2017 17:22

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


      1. yogurt1
        20.03.2017 23:10

        Какой смысл в таких статьях, если они быстро устаревают? Куда проще почитать офф. доки нужной либы
        Лучше бы раскрыли, какой смысл в Mobx
        Мне вот очень нравится Mobx, но смущает его магия. Свободного времени сложно найти на то, что бы разобраться в его магии и разобраться, где стоит использовать мобикс, а где нет. Было бы приятно почитать статью на эту тему


        1. movax10h
          21.03.2017 11:13
          +2

          Смысл mobx — управлять состояниями.

          Куда проще почитать офф. доки

          Свободного времени сложно найти на то, что бы разобраться в его магии

          Я вас правильно понял, что вы можете прочитать доки, но у вас времени нет, но статьи бесполезны?


    1. kahi4
      20.03.2017 17:59
      +8

      MobX, RxJS и Redux являютя, по сути, различными идеями реализации реактивного программирования (pull, push и что-то там особое), каждое из которых дает свое преимущество и недостатки (а еще их часто между собой комбинируют).


      MobX, в отличие от redux, больше подходит для построения MVVM архитектуры приложения и хорошо структуризированного ООП, когда redux стремится в функциональщину и нормальным сторам. За счет того, что он (mobx) скрывает в себе много кода по обновлению компонентов, инкапсулирует логику обновления компонентов в себе (в хорошем mobx приложении вы ни разу не должны прописывать shouldComponentUpdate), при этом перерисовывает только непосредственные зависимости в отличие от перерисовки всего View в редуксе, ускоряет и облегчает разработку, требует меньше оверхеда в виде постоянного создавания всевозможных action и reducer, а так же увеличивает отзывчивость страницы. В теории.


      Я бы даже сказал так: в очень оторванной от действительности теории, покуда на практике красивый код из todo-mvc примера превращается в достаточно запутанный VM слой с геморроем при lazy-загрузке, постоянными лишними перерисовками, которые устанешь вычищать, а так же хитрыми костылями там, где он по какой-то причине предпочел работать не так, как тебе нужно. Зато на нем действительно получается изящная MVVM архитектура.


      1. strannik_k
        26.03.2017 18:01
        +2

        На днях наконец-то нашлось время ознакомиться с Mobx.

        постоянными лишними перерисовками
        Сначала тоже наткнулся на проблему с лишними перерисовками, но потом прочитал, что она решается использованием транзакций с помощью action и runInAction — https://mobx.js.org/refguide/transaction.html. Не знаю, может быть вы их использовали и у вас все-равно были лишние перерисовки по другой причине.

        Насчет архитектуры приложений, использующих MobX.
        Из того, что я прочитал/попробовал, не вижу проблем делать архитектуру с MobX как душе угодно. Главное, что можно сделать любые объекты наблюдаемыми и, использовав соответствующие поля этих объектов в autorun, action, computed или render, автоматически подписаться на их изменения.

        Можно сделать один большой стор, можно несколько маленьких. Можно однонаправленный поток данных, можно MVVM, можно заодно и стейты компонентов заменить на observable объекты. Также нет необходимости писать методы в сторах. Можно оставить только данные, ну и вычисляемые значения. Хотя @computed, как и action можно вынести в любое место программы. Нет необходимости писать их в том же наблюдаемом объекте.

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


    1. indestructable
      20.03.2017 18:54
      -1

      Для меня лично MobX стойко ассоциируется с Knockout и всеми его болячками.


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


  1. stifff
    20.03.2017 19:12

    Глупый, наверное вопрос, но можно ли использовать subj в связке с бэкендом на яве?
    Если да, то как?
    Если нет, то есть ли аналоги, которые можно?


    1. justboris
      20.03.2017 20:05
      +2

      Если у вас бекенд отдает обычный JSON, то какая вам разница, что с ним происходит во фронтенде?


      Что с MobX, что с Redux что с ванильным JS все одинаково. Загрузили данные, что-то обновили, отослали обратно.


      1. stifff
        21.03.2017 13:46

        а если не JSON, но Вебсокеты? Есть ли готовые обёртки для реактивности?


        1. mayorovp
          21.03.2017 13:48

          Вам надо искать не какие-то "обертки для реактивности" — а просто парсеры нужного вам формата. Ну или перейти на JSON, это не так сложно.


          1. stifff
            21.03.2017 15:43

            Сорри, я немного неправильно понял — подумалось, что это про JSON, полученный через Ajax.

            Мне нужна именно магия для реактивности.
            Если она будет работать через JSON, прилетающий по весокету — будет отлично.


            1. mayorovp
              21.03.2017 15:56
              +1

              Кажется, вы все еще не понимаете...


              Данные не несут в себе никакого "запаха". Считанные из файла, прилетевшие через AJAX, прилетевние через веб-сокет, введенные пользователем — все они остаются данными. Библиотека работает с ними одинаково.


              Библиотека умеет хранить данные, менять данные, строить графы зависимостей и уведомлять об изменении данных.


              1. stifff
                21.03.2017 16:16

                Так я и не спрашиваю за эту библиотеку. Я спрашиваю за то, что использовать совместно с ней.


                1. comerc
                  21.03.2017 17:03
                  +1

                  Есть прекрасная статья, как организовать магию реактивности. Но на redux-е. Тынц.


            1. VolCh
              21.03.2017 16:13

              У ваших объектов есть методы-экшены, которые изменяют их состояние, вызывая нужные вам реакции, как и ререндеринг React UI (через mobx-react), так и вызовы сервера для сохранения (задаете явно) — это магия MobX: после экшена уведомить об изменении всех потребителей объекта, о которых вызывающий экшен не знает в общем случае. Эти экшены дергаете откуда хотите по любым событиям хоть UI, хоть ответу http, хоть сообщению WS, хоть по таймеру, любым доступным в JS способом.


        1. justboris
          21.03.2017 13:55
          +2

          Обертки, готовые к чему?


          Все вышеупомянутые библиотеки не делают никаких ограничений на общение с сервером. В этом и их достоинство в общем-то.


          Можно и сокетами, если вам так нужно. Вот как это будет на MobX:


          class LogStore {
            @observable logs = [];
          
            constructor({socket}) {
              socket.on('newLog', this.newLog.bind(this));
            }
          
            @action newLog(newLog) {
               this.logs.unshift(new LogEntry({...newLog }));
            }

          Взято отсюда:


          https://github.com/jeffijoe/logpipe-server/blob/master/src/frontend/app/stores/LogStore.js#L35


          1. mayorovp
            21.03.2017 14:12
            +1

            Вместо вызова bind не лучше ли использовать action.bound?


            1. justboris
              21.03.2017 14:15

              Я не настоящий MobX-ер. Может быть и лучше.


              Что нашел в интернете, то показываю, проект по ссылке не мой.


          1. stifff
            21.03.2017 15:41

            Наверное, я немного плаваю в терминологии и плохо вопрошаю.

            Вот есть куча @observable. Некоторые из них я хочу синхронизировать с сервером. Чтобы при изменении на клиенте они тут же отсылались на сервер. И в обратную сторону — при изменении на сервере обновлялся клиент.

            В вышеупомянутом LogStore это делается руками. Вот есть ли какая-то библиотека, которая автоматизировала бы процесс синхронизации стейтов между сервером и клиентом?


            1. VolCh
              21.03.2017 15:46

              Я для этих целей использую Apollo Client (и Server, но это нюанс), но с MobX он не интегрирован от слова "никак", всё ручками.


            1. justboris
              21.03.2017 15:46
              +1

              В общем виде в библиотеках магического сохранения данных на сервер нет, потому что это сильно завязано на конкретный способ хранения данных и бизнес-логику. В общем виде это не оформить.


              Как вариант, есть Apollo. Это что-то похожее на Meteor, только поверх React и GraphQL. Но, кажется, это не то, что вам нужно.


              1. VolCh
                21.03.2017 15:48

                Если не замахиваться на универсальную "магическую" реализацию, то вполне себе нормально их клиент интегрируется с MobX, включая подписки на обновления по веб-сокетам


                1. stifff
                  21.03.2017 16:19

                  Благодарю, попробую копнуть в сторону Apollo.

                  А за универсальность… Ведь у Метеора получилось довольно универсально, через синхронизацию Монги


                  1. VolCh
                    21.03.2017 17:21

                    Не копал Метеор, но "по слухам" это фулл-стэк решение от фронта до монги. Связка же MobX+React от бэкенда никак не зависит, а Apollo требует реализации GraphQL, а они есть не менее чем на десятке языков.


                    По универсальному MobX+Apollo есть наметки на https://github.com/apollographql/apollo-client/issues/503


                    1. stifff
                      22.03.2017 01:00

                      Вот фулл-стек решение не всегда плохо, мне кажется. Когда нужно на существующий бэкенд добавить реактивности, тогда метеор «не очень». Но когда делается что-то с нуля — ощущается некая разница в сложности подхода.

                      с одной стороны, у нас есть метеор, который в целом работает.
                      с другой стороны «небольшой стек» — Java-GraphQL-Apollo-MobX-React который ещё нужно суметь запустить. Ведь в каждом компоненте из списка есть какой-то нюанс.

                      мне кажется, что порог входа во втором случае на порядок выше, чем в первом. И не понятно почему так. Почему нет более простого решения. Может быть оно никому и не нужно?

                      На сколько я понял, работая с метеором, в нём нет возможности напрямую сделать реактивную переменную/поле — реактивность завязана на Монгу.
                      В длинном стеке же можно сделать реактивную переменную, но ценой довольно слоистой архитектуры. Возможно, из-за этого отличия и получается такая разница в сложности…


                      1. VolCh
                        22.03.2017 11:30
                        +1

                        Что мне нравится в "«небольшой стек» — Java-GraphQL-Apollo-MobX-React", что зависимости в стэке чисто высокоуровневые, любой компонент стэка можно относительно легко заменить на другой, хоть самописный.


            1. movax10h
              21.03.2017 18:02

              Думаю, вы на правильном пути, вам необходим WebSocket. Приблизительно это выглядит так, например, кто-то поменял данные о пользователе, и сервер получил эту информацию. Тогда через сокеты сервер отсылает всем клиентам сообщение вида

               {store:"user", method:"edit", fields:"{id:1, name:"firstname",...}"}
              

              обработчик на клиенте принимает сообщение, парсит что/как нужно изменить и дергает необходимый метод, например:

              userStore.editUser(fields);

              @observable users = [];
              ...
              editUser(fields){
              let user = this.users.filter(user=>{
                return  user.id==fields.id;
              });
              
              user.set(fields);
              }
              


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


              1. stifff
                22.03.2017 01:12

                С «ручным» парсингом прилетевшего обновления — в целом понятно.
                Мне почему-то казалось, что есть готовая библиотека с магией, которая сделает за меня вот эту работу:

                обработчик на клиенте принимает сообщение, парсит что/как нужно изменить и дергает необходимый метод,


                а так же в другую сторону.

                Обновилось, например, на каком-то из клиентов firstname, а об этом сразу узнал сервер и потом все клиенты. сами, без дополнительных телодвижений. А потом эти изменения ещё и отобразились в шаблонах. Сами.

                Походу, такая магия есть только у Метеора. У остальных — только реактивные переменные.


                1. Riim
                  22.03.2017 01:16

                  Не уверен, но вроде то, что вам нужно: gritzko/swarm, josephg/ShareJS.


                  1. comerc
                    22.03.2017 11:10

                    Это заброшенные проекты!


                    1. Riim
                      22.03.2017 13:08

                      Знаете такие же, но не заброшенные?


                      1. comerc
                        22.03.2017 14:41

                        1. comerc
                          22.03.2017 17:14

                          Сегодня выложили видео доклада https://youtu.be/1RMhUPsVw2M


                1. comerc
                  22.03.2017 14:40

                  такая магия есть только у Метеора. У остальных — только реактивные переменные.

                  Внезапно такая магия в Метеоре называется ReactiveVar :)


    1. VolCh
      20.03.2017 20:07
      +1

      Без разницы с чем использовать. Связка mobx+react (как и redux+react, та и вообще любой нормальный фронтенд не делает предположений о том, что на бэкенде). Более того, mobx+react и redux+react вообще с бэкендом не взаимодействуют, они хранят данные в переменных js, изменяют их и отображают изменения. Откуда данные берутся и куда отдаются вне их зоні ответственности.


      1. stifff
        21.03.2017 13:45

        Понял, благодарю.

        А если мне хочется реактивности, подобной Метеору, что из готового взять в дополнении к mobx+react?
        Чтобы при изменении данных на сервере все клиенты видели их сразу.
        Как я понимаю, нужна какая-то обёртка над вебсокетами. Что со стороны фронтенда, что со стороны бэкенда.


        1. mayorovp
          21.03.2017 13:49

          Зачем вам обертка над вебсокетами?


        1. VolCh
          21.03.2017 14:07

          Грубо, есть какой-то объект order


          @observable
          class Order {
            status = 'new';
            @action set status(value) {this.status = value;} 
          }

          В привычном вам обработчике websocket делаете order.status = statusFromWSMessage


          1. mayorovp
            21.03.2017 14:19
            +1

            Нет, так нельзя делать. Декоратор @observable применяется к свойствам, а не к классам.


            Вот так правильно:


            class Order {
              @observable status = 'new';
            }


            1. VolCh
              21.03.2017 15:03

              Ну да. Перепутал с обсервер, с головы когда писал :)


  1. yurtaev
    20.03.2017 20:10
    +1

    MobX выглядит намного "проще" в использование в отличие от redux где необходимо писать больше вспомогательного кода. Но вопрос в том что выбираем, кажущуюся простоту в написание или отсутствие магии в коде с возможностью протестировать почти 100% функционала. Да, не подготовленного человека redux испугает всеми своими actions/reducers/selectors, но в итоге это решает больше проблем чем вносит. Для себя вывел главное преимущество это то что в итоге UI = f(x), где каждый самый маленький компонент можно протестировать изолировано от всей системы, так и весь поток входящих/исходящих данных/событий в целом между компонентами.


    MobX это путь назад к angular с магией под капотом. По началу кажется что вычисляемые значения это круто (привет $watch), и везде хвалят что вот у нас в отличии от redux не нужно с этим мучатся, но в итоге у нас скрытая реализация в объекте с this.some.get('name'), в то время как с reselect name = f(x).


    Ну и самая горячая фича ради которой я готов страдать это тулинг с перемотками состояний. Да в mobx можно отследить изменения объектов, но с redux можно воспроизвести любое состояние приложения (в момент отправки запроса, невалидные формы и т.д).


    В «настоящих» проектах мы получаем данные от сервера или пользовательского ввода, форматируем, валидируем, нормализуем и производим другие операции над ними

    Расскажите как работаете с формами. С redux после того как думаешь что разобрался со всем, вспоминаешь про формы и снова уходишь на пару дней в транс.


    1. comerc
      21.03.2017 00:44

      А в чем печалька с формами? Захотел легкой жизни, добавил в проект redux-form. И тут началось. Выпилил. Вернулся к ручному труду. Собираюсь поделиться навыками, пока выписываю в блокнотик тезисы для заметки.


      1. yurtaev
        21.03.2017 05:48

        redux-form просто прекрасен. Расширяем, очень продуманный интерфейс, в четыре строки можно завернуть свой инпут, валидаторы как функции, а не магические "required|isEmail|isMagic" которые непонятно где. Только опять же высокий порог вхождения, но зато опять же это всё окупается в будущем. mobx-react-form похож, но на сложных кейсах не пробовал.


        1. comerc
          21.03.2017 15:15
          +1

          462 открытых issues как бы намекают. Я не смог себя заставить. Кода для обслуживания получается больше, чем без redux-form. И нужно думать не только о поведении формы, но как ее заставить работать с помощью этой прекрасной обертки. В морг.


          1. yurtaev
            21.03.2017 16:06

            Самое печальное в redux-form что в итоге компонент формы намертво связан с библиотекой. С другой стороны я не представляю как поддерживать руками свои формы, если их много и они сложный (представим CRM систему).


            Вообще вопрос с формами очень важный, и странно что мало качественных решений в react мире (везде?), тот же formsy-react для меня выглядит хуже в плане продуманности API, и вообще решений мало...


            1. comerc
              21.03.2017 17:17
              +2

              А невозможно угодить всем и вся. Это нужна обертка на каждый случай применения. Был у меня подобный печальный опыт с Meteor-овскими велосипедом AutoForm. На первый взгляд — замечательно. Описываешь конфиг и оно само тебе формы выдает! Для двух полей это работает. Но когда формы большие, да со связанными полями. Божечки. Тормозит жутко. Глючит. И опять же вынуждает тебя лезть под капот с кувалдометром. Автор забил на пулл-реквесты. Остаётся форк — вешаешь на саппорт большую кучу "универсального" кода. Оно надо?


    1. movax10h
      21.03.2017 11:32
      +1

      Да, всегда так с формами. Видел вот такую реализацию:
      стор

      export default class FormStore extends ContextStore {
        @observable fields = asMap();
        @observable defaults = asMap();
        @observable errors = asMap();
      ...
        @action updateField = (field, value) => {
          this.fields.set(field, value);
        }
        getFields() {
          return this.fields.toJS();
        }
      
        getErrors() {
          return this.errors.toJS();
        }
      

      и HOC
          <TextField
            type="text"
            placeholder="First Name"
            name="firstName"
            value={props.fields.firstName}
            errorMessage={props.errors.firstName}
            onChange={props.updateField}
            isTransparent={false} />
      


  1. Bellicus
    21.03.2017 02:54

    Минусуйте меня полностью, но не слишком ли дохрена кода для простого открытия меню?


    1. mayorovp
      21.03.2017 09:23
      +2

      Вот код, необходимый и достаточный для открытия меню:


      class MenuStore {
        @observable show;
      
        constructor() {
          this.show = false;
        }
      
        toggleLeftPanel() {
          this.show = !this.show;
        }
      }

      Остальное — разметка и импорты. Что вы видите тут лишнего?


      Ну, конструктор, в принципе, можно и убрать, тут согласен...


  1. pelayo
    26.03.2017 15:17
    +4

    Пробую сейчас mobx у себя на пректе, по сравнению с redux отмечу плюсы:


    1. Гораздо меньше вспомогательного кода. Фокус на бизнес логике, а не рутине.
    2. Не нужен глобальный стор, приложение удобнее масштабировать.
    3. Чтобы сделать при прочих равных redux приложение таким же быстрым как mobx нужно быть Деном Абрамовым.
    4. Полноценный ООП. Чем сложнее приложение, чем больше сущностей и переиспользуемого кода, тем менее годиться redux. Я бы скорее выбирал не между mobx и redux, а между mobx + react и Angular 2.
    5. Простые классы, более "натиный" код.

    Минусы:


    1. Больше магии. Меня не напрягает, оно работает)
    2. Высокие требования к выбору архитектуры. Mobx просто либа, которая не организует потока данных в приложении, в туториалах видел много хардкода.
    3. Пока небольшое комьюнити, особенно на русском.

    В целом статья хорошая, спасибо автору. Со своей строны добавил бы, что не очень нравиться использование стора внутри компонентов, прямой доступ к observerable переменным. В своём приложении я всё это вынес в служебный декоратор, сами компоненты о mobx ничего не знают и получают только чистый js. Не совсем понял смысл использовать класс User, можно было обойтись простой observerable коллекцией, а toggle выест на уровнь выше.