В «настоящих» проектах мы получаем данные от сервера или пользовательского ввода, форматируем, валидируем, нормализуем и производим другие операции над ними. Всё это принято считать бизнес логикой и должно быть помещено в 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.
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 для «гамбургера» в
<div onClick={() => { menuStore.toggleLeftPanel() }} className={styles['toggle-btn']}>☰</div>
и метод toggleLeftPanel() в
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: По дефолту мы экспортируем хранилище как инстанс синглтона, также экспортируется и класс напрямую, так как он тоже может понадобиться, например, для тестов.
Для наглядности добавим стили:
.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 для управления состоянием панели. Давайте немного нарастим мяса и попробуем управлять панелью из другого компонента. Нам потребуются дополнительные методы для открытия и закрытия панели:
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
Создадим еще один компонент
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 страницу. Должно получиться следующее:
В браузере это будет выглядеть так:
Теперь мы можем управлять состоянием панели не только из самой панели, но и из другого компонента.
Note: если несколько раз произвести одно и тоже действие, например, нажать кнопку «close left panel», то в деббагере можно видеть, что экшен срабатывает, но никакой реакции не происходит. Это значит, что mobx не перересовывает компонент, так как состояние не изменилось и нам не нужно писать «лишний» код, как для pure react компонент.
Осталось немного причесать наш подход, работать со сторами приятно, но разбрасывать импорты хранилищ по всему проекту некрасиво. В mobx-react для таких целей появился Provider (см. Provider and inject) — компонент, который позволяет передавать сторы (и не только) потомкам, используя react context. Для этого обернем корневой компонент app.js в Provider:
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 компонент на два, чтобы получился «глупый» и «умный» компонент.
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;
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:
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;
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" для получения пользователей:
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); });
Нарочно добавим задержку, чтобы проверить, что приложение работает корректно даже с большим интервалом ответа сервера.
Далее нам понадобится
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:
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.
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.
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/
Добавить комментарий