Documentation

Documentation versions (currently viewing)

State Management with MobX

Managing application state with the MobX state management library in Hilla.

Application state can be managed in many different ways. In simple applications, you may not need an explicit state management strategy. However, your application may grow to have many views or a complex UI where the same data needs to be displayed in multiple places, or where there are many dependencies between UI controls. In such situations, it may become difficult to keep the application in a consistent state when the user interacts with it. If you don’t have a good state management strategy, it can be easy to introduce new bugs when changing application logic.

This is where a state management library such as MobX can help to make the application easier to maintain and extend. Here, we focus on an approach to store all shared frontend state in a central MobX state store that acts as the single source of truth for the application state. It’s then easy to display the state data, or anything derived from it, anywhere in the UI, so that the data is automatically updated everywhere it’s displayed whenever the data is updated in the state store.

Using MobX in your Application

Hilla recommends using the MobX library to manage frontend state in your applications. When using Lit, you need to extend the MobxLitElement class, instead of LitElement. When your views are based on MobxLitElement, any MobX observables used in the render() method are automatically tracked so that any change to those observables is automatically reflected in the view.

Creating a new Hilla project with the Hilla CLI already takes care of setting up the basics for you by providing a MobX store (named AppState) and View and Layout helper base classes for your application views and layouts. Both View and Layout extend MobxLitElement and you can expand the AppState store with your own observables and actions to be used in your views.

Caution
MobX decorators with TypeScript

MobX has experimental support for decorators (@observable, @action) but using them requires a special setting "useDefineForClassFields": true in tsconfig.json. Using this setting breaks Lit decorators (@property, @query), so you cannot use both Lit decorators and MobX decorators in the same app. For now, we recommend not using MobX decorators in projects that use Lit.

For more information, see Enabling decorators in the MobX documentation.

Creating a Data Store

Here is a minimal example of a MobX store that contains one observable property (quote) and one action (setQuote()) to update that property. Here, the store is initialized and exported on the last line, so that you can import the same store instance into any view or component to access the shared state.

import { makeAutoObservable } from 'mobx';

class MyState {
  quote: string = `Anything that can be derived from the application state,
    should be. Automatically. - MobX documentation`;

  constructor() {
    makeAutoObservable(this);
  }

  setQuote(quote: string) {
    this.quote = quote;
  }
}
export const myState = new MyState();

makeAutoObservable can automatically infer that quote is an observable and setQuote() is an action.

Make sure that an initial value is assigned to all state properties before calling makeAutoObservable(), otherwise the property will not be observable. This is a MobX limitation, since we cannot use the TypeScript configuration "useDefineForClassFields": true due to the conflict with Lit decorators mentioned earlier.

Note
Data Store Definition

"The main responsibility of stores is to move logic and state out of your components into a standalone testable unit." – Defining data stores in MobX documentation.

Using a Store

import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import '@vaadin/text-field';
import type { TextField } from '@vaadin/text-field';
import { MobxLitElement } from '@adobe/lit-mobx';
import { myState } from 'Frontend/store/my-state'; // (1)

@customElement('my-view')
export class MyView extends MobxLitElement { // (2)
  render() {
    const { quote } = myState; // (3)

    // (4)
    return html`
      <p>${quote}</p>
      <vaadin-text-field .value="${quote}" @input="${this.onInput}"></vaadin-text-field>
    `;
  }

  onInput(e: InputEvent) {
    myState.setQuote((e.target as TextField).value); // (5)
  }
}
  1. Import the store into your view or component.

  2. Make sure that the view or component extends View.

  3. Alias one or more state properties from myState into local variables for easy referencing.

  4. Use state properties in the template.

  5. Update state by calling an action method in the store.

By default, the state cannot be changed outside of actions. Read more about actions in the MobX documentation.

Understanding MobX

For information on using MobX effectively and understanding how it works, you should take a look at the official MobX documentation, which is a great resource for learning the basic concepts, as well as advanced usage.

In addition to the concepts of observables and actions that we’ve demonstrated here, you should also be aware of computed values and reactions and when to use them.