Hilla Documentation

Application State Management With MobX

Hilla 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

Hilla applications 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 does not 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, such as Redux, is that it requires almost no boilerplate and it uses normal TypeScript syntax to update 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 Hilla application are rendering the view template and using autorun functions.

MobX uses stores to store state and logic. A store is similar to a controller in the MVC pattern, or a presenter in an 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 from the existing state. For instance, do not store a separate contactCount value, but compute it instead, 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 is best to keep business and application 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 Hilla CRM application has three main stores:

  • AppStore - the 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 add the UiStore later, in the Adding a Login Screen and Authenticating Users chapter.

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 { CrmEndpoint } from 'Frontend/generated/endpoints';

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

Notice that import generated TypeScript types for Contact, Company, and Status. Also import the CrmEndpoint.

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 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 does not create proxy objects for all objects. Also, you do not need the initFromServer() method to be turned into an action.

  3. Options. Enable autoBind, which will bind all actions to this class. This 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 CrmEndpoint.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 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 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 to set the values, or by using runInAction() to explicitly run the state update in an action.

Lastly, replace the contents of frontend/stores/app-store.ts with the following:

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 AppStore is to ensure that you have only 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 work with only 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 do not have the development server running, start it with the mvn command from the command line.