This is part of the Ext JS to React blog series. You can review the code from this article on the Ext JS to React Git repo.
Applications can be very complex and therefore managing the state of an application can be complex. In our previous article, Handling Application State with Redux, we looked at how Redux can be incorporated into a React application as a global state manager. In this article we will strive to accomplish the same end goal. We’ll use the popular state management library MobX to furnish each component with application-level state data. Additionally, we’ll show how components communicate changes from user interaction up to the global application state.
MobX Terms
Before we jump into seeing how to utilize MobX, let’s discuss some terminology:
- action is like an event listener and will modify the state. For example, an
input
would have anonChange
prop that would point to a function that is set up to be an action. If a function does not modify state, it should not be marked to be an action. - computed is a pure function that returns a derived value much like a
ViewModel
’s formula within Ext JS. - observable is a value that when changed will trigger a rerender. An observable can be anything from an object or array to even functions and primitives. Observables can be passed as props from a parent or as class props.
- observer is a component that reacts to changes in an observable. If an observable changes, the component will be rerendered.
- provider is a component that can pass stores (or really whatever you’d like: objects, arrays, strings, etc.) down through an app to all views
- inject is a function that passes global
Provider
data as props to components in the app
To summarize, an action will modify an observable which an observer can automatically track in order to rerender a component and inject passes Provider props to decorated components. Let’s start where the React Binding article left off and transition to using MobX.
Enable Decorators in the Starter App
Before we start applying MobX on top of our existing React example, we need to follow a common code style when using MobX and that is to enable decorators. If you are using create-react-app, you will need to run the npm run eject
command as the generated application will not allow you to add decorator support. Next, we need to install a Babel plugin that will add support for decorators by running:
npm install --saveDev babel-plugin-transform-decorators-legacy
To enable this plugin, we need to edit {appRoot}/package.json
to add the plugin:
"babel": { "plugins": [ "transform-decorators-legacy" ], "presets": [ "react-app" ] },
For more information about decorators and other means of sharing application code, refer to the Mixin article. You can choose to use MobX without using decorator syntax as well. To see how, refer to the MobX documentation.
Before we continue, we need to install mobx and mobx-react:
npm install --save mobx mobx-react
MobX UserStore Class
Our Form
view will read from and write to a global state object instead of to its own component state. We’ll create the global state object as its own UserStore
class. Later in the example code we’ll create a UserStore
instance and pass it to the global Provider
instance, which we’ll inject into our Form
class. The Form
class will also be able to create its own UserStore
instance, in the event one wasn’t injected. In this case, our global state object is quite simple. It’s effectively just an object with an observable user
property. However, in a future article we’ll expand on this class, adding a utility method that is then also shared to all components injected with the UserStore
.
import { observable } from 'mobx'; export default class UserStore { @observable user = {}; }
MobX Global State Provider
The UserStore
class instance is passed as a store
prop on the Provider
wrapping our application. The name of the store prop can be anything you choose. With MobX you’re not restricted to one global state object. You can pass as many global props as suits your particular application. You can even pass a root store with child-stores as its properties. MobX offers quite a bit of flexibility when it comes to organizing your global application state. For our purposes, we’ll stick with a single state object with one property:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import registerServiceWorker from './registerServiceWorker'; import { Provider } from 'mobx-react'; import UserStore from './UserStore'; ReactDOM.render( <Provider store={new UserStore()}> <App /> </Provider>, document.getElementById('root')); registerServiceWorker();
MobX Form Class
With our existing application code, we use React’s built in component state. The examples that follow, however, will use MobX to abstract much of that logic away. We do this by making the Form class an observer, setting a property to hold the user information, and make it an observable. Finally, we will create an action to handle change within the form’s fields. Let’s look at the new Form class:
import React, { Component } from 'react'; import { action, extendObservable, observable } from 'mobx'; import { inject, observer } from 'mobx-react'; import UserStore from './UserStore'; @inject(({ store }, { user }) => { return { store, user }; }) @observer class Form extends Component { static defaultProps = { store: new UserStore(), @observable user: {} } render() { const { user } = this.props; const { renderField, submit } = this; return ( <form> {renderField(user, 'name')} {renderField(user, 'email', undefined, 'email')} {renderField(user, 'phone', 'Phone Number', 'tel')} {renderField(user, 'company')} {renderField(user, 'department')} {renderField(user, 'title')} <button onClick={submit}>Submit</button> <br /> </form> ); } renderField = (user, name, label = name, type = 'text') => { if (user[name] == null) { extendObservable(user, { [name]: '' }); } return ( <div style={{ marginBottom: '12px' }}> <label style={{ textTransform: 'capitalize' }}> {label} <input type="text" name={name} value={user[name]} onChange={this.handleFieldChange} style={{ marginLeft: '12px' }} /> </label> </div> ); } @action.bound handleFieldChange(e) { const { onChange, user } = this.props; const { name, value } = e.target; user[name] = value; if (onChange) { onChange({ [name]: value }); } } submit = e => { e.preventDefault(); // do submit } } export default Form;
MobX Form Explained
It’s important to note the usage of @observer
, @observable
, and @action.bound
. This is how MobX connects everything together. The Form
class is made to be an observer. Meaning, it can now react to any changes to observables. We have an observable for it to react to, which is the user
prop. Observables may be passed as a prop as well. The Form
class will ensure it’s an observable in case what’s passed is not already an observable.
The global UserStore
object is connected to our Form
using the @inject
decorator. The inject
function allows the passing of strings of the Provider
props we’re interested in passing down. So, if all we wanted was the store
prop we could have used @inject('store')
and store
would then exist as a property on the Form
‘s props
object. In this example, we’ve set up the Form
such that a user object with all of the values for each form field could be passed in the form instance or set on the global state object. For our example, we’ll pass in a user
prop on the Form
.
The inject
function’s first param can also be a function, which we use in this example. It’s passed two params that we’ll make use of. The first is an object containing all props set on the wrapping Provider
. We’re interested in the store
prop in this case. The second is any props passed to the Form
instance. In this case, we’re interested in the user
prop. We inject the user
prop from the Form
instance if present. Else, we pass the UserStore
‘s user
property.
Extending the Observable User Prop
The componentDidMount
method sets the user prop on the global state object. The global state object hosts the user data that our Form
(and potentially other components within our app) uses to inform the render process. The store’s user property is passed to the renderField
method called during the render process to create and populate each form field. Inside of renderField
we see the following check:
if (user[name] == null) { extendObservable(user, { [name]: '' }); }
When MobX creates an observable using an object, a change in that object’s values will trigger another render in an observer component. However, new properties added to the observable later will not trigger a response from the observer unless the new properties are added to the observable using the extendObservable
utility method. We do that in the renderField
method so that an empty user object is built up as form fields are created the in the first render pass.
Updating an Observable
In the last example, you may have missed out on an important difference between how MobX performs updates versus how React updates component state. With React, an object or array will only trigger a re-render if what is passed is a different instance. You cannot simply set a property on an object or push something onto an array and have React re-render. With a MobX observable, you can do just that.
You can simply set a property on an object and MobX will trigger the render phase. If you look at the handleFieldChange
method that is marked as an action
, you’ll see this in action as it simply sets a property on the user
observable. As a result, the component will be re-rendered. This way of updating an object without creating a new instance of it can arguably be said to be what a user would expect to trigger a re-render.
MobX Form Example
To populate the form, we pass a user data object with properties matching the name
attributes of the fields in our form.
import React from 'react'; import Form from './Form'; const user = { id: 1, name: 'Don Draper', email: 'don.draper@scdp.com', phone: '+1 (212) 555-0112', company: 'Sterling Cooper Draper Pryce', department: 'Marketing', title: 'Creative Director' }; export default () => <Form user={user}/>;
MobX Computed Values
With Ext JS, the raw data held by a ViewModel is sometimes not enough and you need to use a formula to act upon the data. For example, if your data has a first name and last name separately, but you want a single data node with the names joined, you can join them with a formula like:
Ext.define('MyApp.view.main.MainViewModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.main', data: { user: {...} }, formulas: { name: function (get) { var user = get('user'); return user.firstName + ' ' + user.lastName; } } });
With MobX, you can accomplish this by using the @computed
decorator:
class User extends Component { @observable user = this.props.user || DEFAULT_USER @computed get name () { const { user } = this return `${user.firstName} ${user.lastName}` } render () { return ( <div>{this.name}</div> ) } }
Using Devtools to Inspect the Global State Object
If you’re using Chrome as your browser for development you may want to check out the MobX DevTools plugin. The DevTools plugin makes it easy to inspect the state object at runtime. With the DevTools enabled when you run the example you can open the “MobX” tab on the Chrome DevTools panel and select the “Form …” from the Components tab. The state’s user node can be expanded on the right-hand details pane revealing the data we’re using to populate our form.
Summary
MobX is one of the more popular state management libraries you can choose for your React application. MobX is very intuitive to use, especially coming from Ext JS. Unlike other libraries, you do not need to separate the logic away from the component allowing everything to be contained within a single file. If you find that your application is growing, and would benefit from global state management and you appreciate the event / handler relationship from the Ext JS framework then look no further than MobX.
Mitchell Simoens
Related Posts
-
Ext JS to React: FAQ
This is part of the Ext JS to React blog series. React is Facebook's breakout…
-
Ext JS to React: Migration to Open Source
Worried about Migrating from Ext JS? Modus has the Answers Idera’s acquisition of Sencha has…