The art of state, Part 1: Introduction to the client state

One of the major changes in Mendix 7 was the introduction of stateless runtime, where the state was moved from the runtime to the client. In Mendix 7, the client now keeps track of the state and sends it to the runtime along with the requests for both the browser and mobile app. This feature allows the runtime to scale horizontally, providing immense flexibility when an application needs more server resources.

In part one of this blog series, we’ll go through an app to show you what is happening from a state perspective at each step so you can gain a better understanding of how the client state works.

Disclaimer: This blog post applies to Mendix 7.23.1. State behavior may differ in previous releases and may be changed without notice in further releases.

What is the state and what does it actually contain?

As a user of a Mendix app, some of the data you work with may not yet be stored in the database. To be precise, the state consists of:

  • All newly created and not-yet-committed persistable objects (PEs)
  • All unpersistable objects (NPEs)
  • All attribute and association changes made to the objects (associations and attributes are treated the same in the client, so associations will not be mentioned for the rest of this post)

These objects and changes comprise the state. Since they are not yet stored in your app’s database, you need to store them in the state until they are stored in the database, discarded, or no longer needed in the app.

Modifications to the state can be introduced in different ways:

  • A new object can be created by a microflow in the runtime, a nanoflow in the client, or by a Create object client action
  • An attribute change can be made by a microflow, nanoflow, custom widget, or by the end user

From the state’s perspective, the source of the object or change does not matter. All objects and changes end up in the client state.

The state might contain more objects than previously outlined. Sometimes the state even contains committed objects. This is often done to increase an app’s performance.

What is a change and how is it stored in the state?

Changes refer to the uncommitted values of Mendix objects’ attributes. For example, when a user changes an attribute’s value using a textbox, Mendix stores the new value as a change in the edited object’s state. The Mendix object itself is not yet changed.

Keeping the changes separate from Mendix objects makes rollbacks possible. Whenever you rollback a Mendix object in a microflow, nanoflow, or with a cancel changes client action, Mendix just discards all the changes made for the object in the state.

This means that whenever a user clicks a save button, Mendix collects and sends all the changes made to the edited object so those changes can be saved to the runtime’s database. This is also true for other runtime requests which require a changed object, such as microflow calls.

Scope of the state

With Mendix 7, the state is stored in a browser’s memory by the Mendix Client. This means that for web apps, the state is local to the current browser tab. If you open your app in a separate tab, you will not be able to access the state of the previous tab. This also means that refreshing the current browser tab will cause its state to be lost.

There is one exception to the last case. The Mendix Client stores your state to the session storage of the browser momentarily for our fast deploy feature. This allows you to continue working within your app from the page or state you were in when you made changes to your model, and then run it. This feature is available only during development.

Both behaviors above are new to Mendix 7 and are incompatible with apps built in Mendix 6 and earlier. In Mendix 6, the state is still accessible from different tabs and will survive refreshes.

State and security

Storing the state on the client has some security ramifications:

Read-only attributes for current user- Mendix protects read-only attribute values for a current user with a hash next to those values, so any attempt to change those values illegally is caught and denied.

Changes for inaccessible attributes for current user- Changes to the attributes which the logged-in user cannot access cannot be stored in the state. Sending them to the client risks disclosing confidential data. So, such changes are discarded.

How is state communicated to runtime?

The Mendix Client communicates with the runtime through a special endpoint /xas, for example when loading data for a data grid or calling a microflow. Each type of call to this API is called an action and necessary parts of the state are sent along with it. You can inspect xas requests in the Network tab of your browser’s developer tools and filtering out requests to the xas path. We will visit xas API later in the post.

Disclaimer: xas is a private API between the Mendix Client and runtime and subject to change in future releases without notice. Details provided here aim to increase the understanding about how Mendix apps communicate with the runtime so you can model better apps and troubleshoot app issues more effectively.

Whenever a xas action (such as a microflow call) is triggered, Mendix Client also sends the state. Mendix Client does not send the entire state to the runtime, as this would create major performance issues. Mendix decides which parts of the state should be sent by analyzing each microflow during the deployment of the applications.

Can I inspect the client state?

Pressing the Ctrl+Alt+G key combination dumps a summary of the client state to the browser console at that moment, so you can inspect what is stored. We will use this shortcut to inspect our example app.

The artwork registry app

To demonstrate the client state, I built a simple artwork registry app where information about artists can be stored.

You can download the project here and inspect the state as we go through it.

Here is our simple domain model. There is an Artist entity with a FullName attribute.

Let’s run our app. The home page is empty on purpose, so that the Mendix Client starts with an empty state.

We can verify this by opening Console in developer tools and pressing the Ctrl+Alt+G shortcut.

Here you can see that the state is represented as a JSON object, but it is currently empty.

Click the Artists menu item. Here there is a list view of all artists defined in the app. Currently, there are none.

Create a new artist by clicking the Create New Artist button and inspect what happens in the Network tab of the developer tools. Select the first request to the xas/ path and scroll down on the Headers tab in the detail view:

Extract the request payload and inspect:

Switch to "Text" tab and paste code here

Above you see a typical xas request. Let’s go through each field:

  • action: We mentioned that all runtime operations go through a xas API, so each operation must be distinguishable. Here the action field serves this purpose. The current action is instantiate, which asks runtime to create a Mendix object.
  • params: Each xas action might have its own parameter set, so params contain the action-specific parameters. For an instantiate call, the runtime needs to know what type of object to create, so it is passed in the objecttype field.
  • changes: This is one of the fields related to the client state. Whenever a xas request is made, the Mendix Client sends the relevant state with the request and changes is a part of that state. Since creating a new object does not require any state, here it is empty.
  • objects: This is the second field related to the client state. Here the Mendix Client supplies objects from the state that the runtime might need during the request. Since creating a new object does not require any object state, it is also empty.

Check the response by selecting the Preview tab within the Network request detail view:

While examining detail, I will skip the screenshots of the developer tools and just show the extracted versions:


{
  "actionResult": "5629499534213124",
  "commits": [],
  "changes": {},
  "resets": {},
  "deletes": [],
  "newpersistable": [
    "5629499534213124"
  ],
  "objects": [
  {
    "objectType": "MyFirstModule.Artist",
    "guid": "5629499534213124",
    "hash": "Nw09VTjCQKib7DbE5Agxgm3Bg6fEGL4mPRyNrgiUKGs=",
    "attributes": {
      "FullName": {
        "value": null
        }
      }
    }
  ]
}

Above you see a typical xas response. The actionResult field is related directly to the action itself, but the other fields are related to the client state.

After each call to the xas, the runtime response retains information about what has happened during the request, like which objects were created or deleted. The runtime communicates them in each response to the client, so the client applies those changes to its state.

Here is an overview of the response fields related to the client state:

Commits

This field contains a list of guids which were committed during the request. Since creating itself does not commit the object, it is empty. It looks like this when it is populated:

"commits": ["guid_1", "guid_2", "...guid_n"]

The Mendix Client uses this information to know when a new object was committed or not. This way it can update client state accordingly.

Changes

This field contains a summary of all the changes to the Mendix objects that were made during the request. This means that those changes have not been committed yet, so they should be stored in the state.

This field is not populated in the instantiate call, since there are no changes for the new Artist object yet. But if the Artist object had an After Create event microflow which changed the name of the artist to a default value, that field would have looked like so:


"changes": {
  "5629499534213124": {
    "FullName": {
      "value": "Value set in the event microflow"
    }
  }
}

The Mendix Client takes changes here and applies them to the client state.

Resets

This field contains a summary of all the rolled back attribute changes for each Mendix object. Mendix Client uses that information to remove the existing changes from its state. Here is a sample of what resets look like:


"resets": {
  "guid_of_the_object": ["attribute_1", "attribute_2"]
}

Deletes

This field contains a list of guids of Mendix objects that were deleted during the request. The Mendix Client uses that deletion information to remove corresponding objects and their changes from the state. This field is empty in the example request above, but here is a sample of what deletes look like:


  "deletes": ["guid_1", "guid_2", "...guid_n"]

Newpersistable

This field contains a list of guids that were created during the request, so the Mendix Client knows that they are not committed yet, and it needs to store them as long as necessary. In our instantiate call, it created a new Artist object, but did not commit. That is why we see that its guid is part of this field.

Objects

This field contains a list of Mendix objects that the runtime action needs to send to the client as a part of the response. In our example, the runtime sends the JSON representation of the new Artist object.

A typical Mendix object may have the following fields:

  • objectType: defines the object type of the object.
  • guid: unique id of the object.
  • hash: this makes sure that the whole object content is not tampered.
  • attributes: contains the committed values for each attribute of the object. For a new object, those values equal to the default values of the attributes. Please note that only the attributes which are accessible to the current user are present.

Let’s return to our demo application after this short introduction to xas request and response.

Clicking the new button created a new instance of the ‘Artist’ entity, and the Mendix Client shows the detail page with the new object:

Here we can now use our state inspection shortcut (Ctrl+Alt+G) to see the client state:


{
  "MyFirstModule.Artist": {
    "5629499534213124 (new)": {
      "subscribedWidgets": [
        "MyFirstModule.Artist_NewEdit.dataView1",
        "MyFirstModule.Artist_NewEdit.textBox1",
        null
      ]
    }
  }
}

The client state now has the new Mendix object, and it represents it by adding a (new) prefix after its guid to indicate that it has not been committed yet. It also has a subscribedWidgets field, which indicates the widgets that use that object. That is why this object is kept on the state. The last entry is null, meaning that there is one more component on the page or in the Mendix Client itself which uses the object, but does not have a description.

Change the name of the new artist to Pablo Picasso, then inspect the state again:


{
  "MyFirstModule.Artist": {
    "5629499534213124 (new)": {
      "changes": {
        "FullName": {
          "value": "Pablo Picasso"
          }
        },
        "subscribedWidgets": [
          "MyFirstModule.Artist_NewEdit.dataView1",
          "MyFirstModule.Artist_NewEdit.textBox1",
          null
        ]
      }
    }
  }
}

This time we see that there is an extra changes field for the object, where it shows the change we made to the FullName field.

At this moment, Mendix Client stores both the initial and the changed value of the FullName field. You can check this by requesting the object from console with mx.data.get and comparing the values returned from MxObject.get and MxObject.getOriginalValue methods:

Save this artist by clicking the Save button, and inspect the request it triggers (some fields omitted for clarity):


{
  "action": "commit",
  "params": {
    "guids": [
      "5629499534213124"
    ]
  },
  "changes": {
    "5629499534213124": {
      "FullName": {
        "value": "Pablo Picasso"
      }
    }
  },
  "objects": [{
    "objectType": "MyFirstModule.Artist",
    "guid": "5629499534213124",
    "hash": "31doiDAq6u7/rMnqwWno01jhLkhpeQ+vKU/rI+IQor8=",
    "attributes": {
      "FullName": {
        "value": null
      }
    }
  }]
}

This time a commit action request is made to runtime, containing the guid of the object to commit to database in the params field. Also the changes field contains the Pablo Picasso change we made, and objects contains the Mendix object we created earlier.

Here is the response:


{
  "commits": [
    "5629499534213124"
  ],
  "changes": {},
  "resets": {
    "5629499534213124": [
      "FullName"
    ]
  },
  "deletes": [],
  "newpersistable": [],
  "objects": [{
    "objectType": "MyFirstModule.Artist",
    "guid": "5629499534213124",
    "hash": "31doiDAq6u7/rMnqwWno01jhLkhpeQ+vKU/rI+IQor8=",
    "attributes": {
      "FullName": {
        "value": "Pablo Picasso"
      }
    }
  }]
}

There is not an actionResult field this time, but there are many changes for the state:

  • The commits field now contains the guid of the Artist we have just committed, so the client state knows that it is not a new object anymore.
  • The resets field contains the guid of the Artist and mentions that FullName field change is removed (because it is now committed). The client can now remove this change from the state.

The objects field now represents the latest version of the committed object, with the FullName field value set to Pablo Picasso.

Why does runtime return the committed object again?

The client already has the object in its state and could reuse it, so why does the runtime return it again? There are two reasons:

  • the object might have an event handler which has further modified the object
  • the object might have a calculated attribute, and its value might have changed when it was committed

That is why the client needs the latest values of the object, and why it is returned from the runtime.

Now your application looks like this:

If you inspect the state now, you will see that Pablo Picasso is still in the state, because it is used by the list view. But there is a small difference: it is not marked as new anymore.


{
  "5629499534213124": {
  "subscribedWidgets": [
    "MyFirstModule.Artist_Overview.listView1",
    "MyFirstModule.Artist_Overview.textBox1"
    ]
  }
}

The Mendix Client also uses the client state as a form of caching layer. When an object is in the state and it is needed by a widget, it is not retrieved from the runtime again. Click the Edit Artist button for “Pablo Picasso” and check the Network tab of devtools. You’ll notice that no new xas requests will be made to retrieve Pablo Picasso from the runtime. Because it is already in the state, there is no need to request it.

Now let’s click Cancel on the edit page, go to home page of our application by clicking Home menu item, and check the state:


{
  "MyFirstModule.Artist": {
    "5629499534213124": "Going to be garbage collected †"
  }
}

Check the state about 15 seconds later. You will see that it is empty.

But why is that?

The answer to this question lies in a new concept: garbage collection from the state. This mechanism detects and removes unnecessary objects from state, so our application performs at its best no matter how many objects you create or use. In our application, the Pablo Picasso object is no longer needed since we opened the home page, because there are no widgets showing it. That is why it is removed from the state. Garbage collection from the state is an elaborate topic, however, so I will cover it in the second part of this blog series.

I hope you enjoyed reading this post and find it helpful. See you later for Part 2!

Unlock the full capacities of Mendix 7 - Try Now