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 axas
API, so each operation must be distinguishable. Here theaction
field serves this purpose. The current action isinstantiate
, which asks runtime to create a Mendix object.params
: Eachxas
action might have its own parameter set, soparams
contain the action-specific parameters. For aninstantiate
call, the runtime needs to know what type of object to create, so it is passed in theobjecttype
field.changes
: This is one of the fields related to the client state. Whenever axas
request is made, the Mendix Client sends the relevant state with the request andchanges
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 theguid
of theArtist
we have just committed, so the client state knows that it is not a new object anymore. - The
resets
field contains theguid
of theArtist
and mentions thatFullName
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!