Application State Management With MobX

Vaadin Fusion uses a reactive programming model for views and components. In a reactive UI programming model, the view has a declarative template that takes in a state and produces the HTML that should be rendered. In programming terms, you can think of the process as:

renderedHtml = template(state)

That is: the view is a function of the state. Any time the state changes, the rendered HTML is updated.

This chapter covers:

  • Application state management with MobX.

  • Creating MobX stores.

  • Fetching endpoint data and saving it in a store.

  • Using application state in a template.

Component vs Application State

Vaadin Fusion application can store state in two places: component properties (@property or @state) or MobX stores. Use MobX stores for application state and state that is derived from the application state.

Component properties are appropriate for storing state that is only relevant to a single component and doesn’t depend on the application state.

An Introduction to State Management With MobX

MobX is a simple and scalable state management library that is used widely in the Lit and React communities.

What makes MobX stand out among other state management libraries, like Redux, is that it requires almost no boilerplate and it uses normal TypeScript syntax for updating state. This makes it easy to learn and use.

At the core of MobX is the observable state: the state that gets updated through actions (methods) that get triggered by events. When the state changes, computed values are updated, and lastly any side-effects are run. The most common side effects in a Fusion application are rendering the view template and using autorun functions.

MobX uses stores for storing state and logic. A store is similar to a controller in the MVC pattern or a presenter in a MVP pattern. Stores can be composed of other stores and can be extended dynamically.

Try to keep the state as small as possible and avoid duplication. Use computed properties to derive values out of the existing state. For instance: don’t store a separate contactCount value, rather compute this based on the contacts array.

MobX caches computed values and only evaluates them if they are used, and if any observables they depend on have changed since they were last computed.

MobX recommends using at least two stores: one for domain objects and one for the application state. It’s best to keep business and app state separate for easier testing and reuse. These two stores are managed by a central store called the AppStore.

Creating The CRM Application State Stores

The Vaadin CRM application has three main stores:

  • AppStore - Container for other stores.

    • UiStore - Application state (logged in, offline).

    • CrmStore - Domain objects and logic (contacts, companies, states).

For now, you only need the AppStore and CrmStore. You will add the UiStore later in the Adding a login screen and authentication chapter. As you only have a single-domain store now, you don’t need an AppStore yet, but you can add it now to avoid having to refactor code later on.

Begin by creating the CrmStore to manage domain objects. Create a new file, frontend/stores/crm-store.ts with the following content.

import { makeAutoObservable, observable, runInAction } from 'mobx';

import Company from 'Frontend/generated/com/example/application/data/entity/Company';
import Contact from 'Frontend/generated/com/example/application/data/entity/Contact';
import Status from 'Frontend/generated/com/example/application/data/entity/Status';
import * as endpoint from 'Frontend/generated/CrmEndpoint';

export class CrmStore {
 contacts: Contact[] = [];
 companies: Company[] = [];
 statuses: Status[] = [];
}

Notice that import generated TypeScript types for Contact, Company, and Status. Also import all methods from CrmEndpoint as endpoint.

The state consists of three arrays: contacts, companies, and statuses.

Turn the class into a MobX observable (store) by calling makeAutoObservable in the constructor:

constructor() {
 makeAutoObservable(
   this,
   {
     initFromServer: false,
     contacts: observable.shallow,
     companies: observable.shallow,
     statuses: observable.shallow,
   },
   { autoBind: true }
 );

 this.initFromServer();
}

makeAutoObservable takes in three arguments:

  1. The observable class.

  2. Overrides. By default, all fields are made into observed values, all functions into actions, and all getters into computed values. In this case, you only want to observe changes to the array content, not the content of the objects in the array. This way, MobX doesn’t create proxy objects for all objects. You also don’t need the initFromServer method to be turned into an action.

  3. Options. Enable autoBind, which will bind all actions to this class. It makes it easier to use them in listeners later on.

Add a method that initializes the store from the server.

async initFromServer() {
 const data = await endpoint.getCrmData();

 runInAction(() => {
   this.contacts = data.contacts;
   this.companies = data.companies;
   this.statuses = data.statuses;
 });
}

initFromServer is an async method. async methods can use the await keyword to suspend the execution until a Promise resolves. async methods make it easier to write non-blocking asynchronous code.

Observables need to be updated through actions. Normally, all methods on the store are actions. But asynchronous code needs to be handled slightly differently. Because the await keyword causes the execution to suspend, the original action is no longer active when the value is returned. You can work around this by either having a separate method just for setting the values, or by using runInAction to explicitly run the state update in an action.

Lastly, add the following to `frontend/stores/app-store.ts:

import { CrmStore } from "./crm-store";

export class AppStore {
 crmStore = new CrmStore();
}

export const appStore = new AppStore();
export const crmStore = appStore.crmStore;

The purpose of the app store is to ensure that you only have one instance of the stores and that they are in sync. Export the crmStore member for convenience. This way, you can import and use crmStore instead of appStore.crmStore, while still ensuring that you only work with one set of stores.

Using a MobX Store From a View Template

Now that you have a store that contains the state, you can use it to display contacts in the list view grid.

First, import the store into the list view:

import { crmStore } from 'Frontend/stores/app-store';

Next, update the template. Use a property binding on vaadin-grid to bind the contacts state to the items property.

<vaadin-grid class="grid h-full" .items="${crmStore.contacts}">

In your browser, you should now see all the contacts listed in the grid. If you don’t have the development server running, start it with the mvn command from the command line.