In “real” projects, we receive data from the server or user input, format, validate, normalize, and perform other operations on them. All this is considered business logic and should be placed in the
M odel. Since react is only a third of M
V C pie, to create user interfaces, we will need something else for business logic. Some use
redux or
flux patterns, some
backbone.js or even angular, we will use
mobx.js as
M odel.
In the previous article we have already prepared the
foundation , we will build on it. Since mobx is a standalone library, then for connecting with react, we need
mobx-react :
npm i --save mobx mobx-react
In addition, to work with decorators and transform class properties, we will need babel
babel-plugin-transform-class-properties and
babel-plugin-transform-decorators-legacy plugins :
')
npm i --save-dev babel-plugin-transform-decorators-legacy babel-plugin-transform-class-properties
Do not forget to add them to .babelrc
"plugins": [ "react-hot-loader/babel", "transform-decorators-legacy", "transform-class-properties" ]
We have a Menu component, let's continue working with it. The panel will have two states “open / closed”, and we will manage the state using mobx.
1. First, we need to
determine the state and make it observable by adding the @observable decorator. A state can be represented by any data structure: objects, arrays, classes, and so on. Create a storage for the menu (menu-store.js) in the stores directory.
import { observable} from 'mobx'; class MenuStore { @observable show; constructor() { this.show = false; } } export default new MenuStore();
Store is an ES6 class with a single show property. We hung decorator @observable on it, thus told the mobx to watch it. Show is the state of our panel, which we will change.
2.
Create a view that responds to state changes . It's good that we already have it, this is component / menu / index.js. Now that the state is changing, our menu will automatically replicate, while mobx will find the shortest way to update the view. To make this happen, you need to wrap the function describing the react component in observer.
components / menu / index.js import React from 'react'; import cn from 'classnames'; import { observer } from 'mobx-react'; import menuStore from '../../stores/menu-store'; 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;
In any react application, we need the
classnames utility to work with className. Previously, it was included in the react-a package, but now it is put separately:
npm i --save classnames
With its help, you can glue the names of classes using various conditions, an indispensable thing.
It can be seen that we add the class “active”, if the value of the menu state is show === true. If in the storage designer you change the state to this.show = true, then the panel will display the “active” class.
3. It remains
to change the state . Add a click event for the hamburger in
menu / index.js <div onClick={() => { menuStore.toggleLeftPanel() }} className={styles['toggle-btn']}>☰</div>
and the toggleLeftPanel () method in
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: By default, we export the storage as a singleton instance, and the class is also exported directly, as it may also be needed, for example, for tests.
For clarity, add styles:
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; } }
And check that by clicking on the icon, our panel opens and closes. We wrote a minimal mobx store to manage the status of the panel. Let's increase the meat a bit and try to control the panel from another component. We will need additional methods for opening and closing the panel:
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 };
You may notice that we added computed and action decorators, they are only required in strict mode (disabled by default). Computed values will be automatically recalculated when the corresponding data is changed. It is recommended to use action, it will help to better structure the application and optimize performance. As you can see, the first argument is the expanded name of the action being performed. Now with debbage we will be able to observe which method was called and how the state changed.
Note: When developing it is convenient to use chrome extensions for mobx and react , as well as react-mobx devtools
Create another component
components / left-panel-controller / index.js import React from 'react'; import menuStore from '../../stores/menu-store'; 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;
Inside are a couple of buttons that will open and close the panel. This component will add to the Home page. You should have the following:
In the browser, it will look like this:
Now we can manage the state of the panel not only from the panel itself, but also from another component.
Note: if you perform the same action several times, for example, press the “close left panel” button, then in the debager you can see that the action is triggered, but no reaction occurs. This means that mobx does not recalculate the component, as the state has not changed and we do not need to write “extra” code, as for the pure react component.
It remains to comb our approach a little, it is pleasant to work with the stores, but it is ugly to scatter imports of storage facilities throughout the project. In mobx-react,
Provider appeared for such purposes
(see Provider and inject) - a component that allows you to pass stors (and not only) to descendants using the
react context . To do this, wrap the app.js root component in the Provider:
app.js import React from 'react'; import { Provider } from 'mobx-react'; import { useStrict } from 'mobx'; import Menu from '../components/menu'; import leftMenuStore from '../stores/menu-store'; 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;
Immediately we import all the stores (we have one) and transfer them to the provider via props. Since the provider works with the context, the stories will be available in any child component. Also, split the menu.js component into two to get a
“stupid” and “smart” component .
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;
“Stupid” is not interesting to us, as it is a normal stateless component that receives data through props about whether the panel and the callback to switch are opened or closed.
It is much more interesting to look at his wrapper: here we see the
HOC , where we inject the necessary storus, in our case “leftMenuStore”, we pass our “stupid component” wrapped in observer as a component. Since we leftMenuStore, the storage is now available via props.
almost the same thing we do with the left-panel-controller:
components / left-menu-controller / left-menu-controller.js import React from 'react'; 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;
With the only difference that we do not use observer here, since this component does not need to redraw, we only need the openLeftPanel () and closeLeftPanel () methods from the storage.
Note: I use displayName to set the name of the component, which is convenient for the debbag:
For example, you can now find a component through the search.
It's all simple, now let's get the data from the server, let it be a list of users with checkboxes.
Go to the server and add the route "/ users" to get 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); });
Deliberately add a delay to verify that the application is working correctly even with a large server response interval.
Next we need
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 };
Here is described the User class with the user property. In mobx there is an
observable.map data type, it is just suitable for us to describe the user. Roughly speaking, we get the observed object, and, moreover, one can observe the change in a specific field. Also getter, setter and other auxiliary methods become available. For example, in the constructor using “merge”, we can easily copy the fields from userData to user. This is very convenient if the object contains many fields. We also write one action to switch the user's state and a calculated value to get information about the user.
The following is the story itself, in which the observed are an array of users. In the constructor, we pull the method to get users from the server and through the action putUsers we fill the empty array with users. Finally, we add a method that returns the calculated number of checked users.
Note: autorun performs the function automatically if the observed value has been changed. For example, all users are displayed in the console. If you try to get users using the “getUsers ()” method, you will notice that the type of the returned data is not Array, but ObservableArray. To convert observable objects into a javascript structure, use toJS () .
In app.js, let's not forget to add a new user store so that descendants can use it.
Add the react components to the components directory:
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;
There is already a familiar wrapper to us, passing an array of users and the number of users checked through props.
user-list / user-list.js import React from 'react'; import UserListItem from './user-list-item'; 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;
We show a list of users and information on their number. Pass the “toggle ()” method of stor via 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;
Render one user.
We add styles and we catch the finished component on the Home page. Everything is ready (
github ), you can play with the checkboxes and make sure that all the methods work.
As a result, we saw how mobx works in conjunction with react, taking into account all the possibilities of mobx, we can assume that such a solution has the right to life. Mobx copes with the responsibility of the state manager for react applications and provides rich functionality for implementation.