React + mobx путь с нуля. Mobx + react, взгляд со стороны

от автора

В «настоящих» проектах мы получаем данные от сервера или пользовательского ввода, форматируем, валидируем, нормализуем и производим другие операции над ними. Всё это принято считать бизнес логикой и должно быть помещено в 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.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 приложений и предоставляет богатый функционал для реализации.
ссылка на оригинал статьи https://habrahabr.ru/post/324388/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *