Skip to content

Data contexts

Mateu provides four distinct data contexts accessible in the browser via ${...} expressions:

ContextScopeUpdated by
stateComponent — form fieldsAction returning this, State(obj), or State(Map)
dataComponent — extra dataAction returning Data(obj) or Data(Map)
appStateApplication — persists across navigationAction returning AppState(map)
appDataApplication — shared transient dataAction returning AppData(map)

state holds the field values of the current form. It is updated when an action returns the component instance or a State object.

// Reading from state in a component
new Text("${JSON.stringify(state)}")
new Text("${state.count}")
// Updating state from an action
@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
return new State(Map.of("something", UUID.randomUUID().toString().substring(24)));
}

The state context is isolated to the current component.


data is a secondary data bag for the current component. It does not map to form fields — it is purely for display or rule expressions.

// Reading from data in a component
new Text("${JSON.stringify(data)}")
new Text("${data.something}")
// Updating data from an action
@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
return new Data(Map.of("something", UUID.randomUUID().toString().substring(24)));
}

Use data for values that should not appear as editable form fields (computed results, metadata, display-only content).


A single action can only return one value. To update both, use List.of(...):

@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
if ("change-state".equals(actionId)) {
return new State(Map.of("something", randomShort()));
}
if ("change-data".equals(actionId)) {
return new Data(Map.of("something", randomShort()));
}
return null;
}

In a component that displays both:

Form.builder()
.content(List.of(
new Text("State: ${JSON.stringify(state)}"),
new Text("Data: ${JSON.stringify(data)}"),
Button.builder().actionId("change-state").label("Change state").build(),
Button.builder().actionId("change-data").label("Change data").build()
))
.build()

appState is shared across all components in the application and persists across navigation. Think of it as a session-scoped store.

// Read appState in a component
new Text("${JSON.stringify(appState)}")
// Update appState from an action — merge new values into the existing map
@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
var appState = new HashMap<>(httpRequest.getAppState(Map.class));
var config = new HashMap<>((Map) appState.getOrDefault("config", new HashMap<>()));
config.put("tenantId", UUID.randomUUID().toString());
appState.put("config", config);
return new AppState(appState);
}

Use httpRequest.getAppState(Class) to read the current appState value inside handleAction.

Use appState for:

  • Authentication tokens or user identity
  • Tenant configuration
  • Application-wide settings that should survive page transitions

appData — application-wide transient data

Section titled “appData — application-wide transient data”

appData is similar to appState but is intended for transient, non-persisted shared data. It is accessible across all components.

// Read appData in a component
new Text("${JSON.stringify(appData)}")
// Update appData from an action
@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
return new AppData(Map.of("random-data", UUID.randomUUID().toString()));
}

AppData updates propagate to all components on the page, including nested ones.


A nested component (a ComponentTreeSupplier embedded in another’s content list) can update appData or appState and the parent will see the change:

// Parent page — embeds NestedForm in its content
Form.builder()
.content(List.of(
new Text("${JSON.stringify(appData)}"),
new NestedForm()
))
.build()
// NestedForm — updates appData
class NestedForm implements ComponentTreeSupplier, ActionHandler {
@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
return new AppData(Map.of("random-data", UUID.randomUUID().toString()));
}
// ...
}

After the nested form fires its action, appData is updated and all components re-render their ${appData.*} expressions.


ExpressionReads fromUpdated by
${state.field}Component statereturn this, new State(obj)
${data.field}Component data bagnew Data(obj)
${appState.field}App-wide statenew AppState(map)
${appData.field}App-wide datanew AppData(map)