Ext JS to React: Handling Application State with MobX

   JavaScript
Ext JS to React: Handling Application State with MobX

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 an onChange 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}/>;


Ext JS to React: Handling Application State with MobX


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.

Ext JS to React: Handling Application State with MobX, DevTools


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.


This website uses cookies

These cookies are used to collect information about how you interact with our website and allow us to remember you. We use this information in order to improve and customize your browsing experience, and for analytics and metrics about our visitors both on this website and other media. To find out more about the cookies we use, see our Privacy Policy.

Please consent to the use of cookies before continuing to browse our site.

Like What You See?

Got any questions?


>