Data Hub
Why The Data Hub Exists
App composer components often need lightweight shared state for itself, boardlet to boardlet communication (app scope) or app to app communication(global). The Data Hub provides:
- Zero‑config ephemeral state (in‑memory, auto‑clears on reload)
- Simple key/value API with three intention‑revealing scopes (Local → App → Global) to minimise accidental coupling
- Deterministic merge & precedence so templates stay concise (
{{ someKey}}just works with local override semantics) - Event reactivity through
ON_DATA_HUB_CHANGEso elements can react declaratively when relevant data changes - A uniform contract consumed by the Dynamic Rendering Context so all dynamic elements read state the same way
Use it whenever you need transient cross‑action or cross‑element data that does not warrant persistence or a domain signal store.
When To Use (Decision Guide)
Choose the smallest scope that solves the problem:
Local: per-content, lightweight UI state (e.g., selected item, local form state, local data used only by one component).
- Example: a card collection storing the currently selected card/item.
App: shared state for communication within one micro-app (e.g., between a dashboard and one or more boardlets).
- Example: a shared filter value used by multiple widgets on the same dashboard.
Global: very common session-wide state and communication across app contexts.
- Example: data that needs to be visible after navigating from one app to another.
Global parameters: data that should be stored in the URL (so it can be shared/bookmarked and survive refresh).
- Example: an
id,tab, orfiltervalue that you want to appear as a query parameter.
- Example: an
Rule of thumb: Start as Local, promote to App only when two or more contents collaborate, promote to Global only if truly cross‑app.
Core Mental Model
The Data Hub maintains three maps at once:
- Local (scoped to a specific
contentId) - App (scoped to the current
appKey) - Global (one map for the whole session)
What matters most is how a value is resolved when you reference it:
Scoped access is exact (no fallback):
{{ local.currentSelection }}means “readcurrentSelectionfrom the Local map only”.- If
currentSelectionis not set in Local,local.currentSelectionisundefinedeven ifapp.currentSelectionorglobal.currentSelectionexists.
Unscoped access follows precedence (with fallback):
{{ currentSelection }}resolves in this order: Local → App → Global.
Some keys can then fall back to the main context object:
- For example,
{{ nodeId }}is resolved as: Local.nodeId → App.nodeId → Global.nodeId → main context’snodeId. - If you want to avoid any fallback/override surprises, use the scoped form (
local.,app.,global.).
- For example,
Practical takeaway: use unscoped keys ({{ key }}) when you want Local to override App/Global; use scoped keys when you need a guaranteed scope.
How To Set Values (Action Editor)
Chain Dynamic Actions:
| Action (Editor) | Scope Impact |
|---|---|
| Local Data Hub: Set Value | Mutates Local map for target contentId (calculated automatically) |
| App Data Hub: Set Value | Mutates App map for inferred appKey |
| Global Data Hub: Set Value | Mutates Global map |
Each Set action defines a Parameters Map (rows). For each row you specify: Target Key, Source Type, Value/Expression.
Supported Source Patterns
| Source Type | Example Input | Result |
|---|---|---|
| Literal | true | boolean true |
Literal + {{variables}} | Username {{global.currentUser.name}} | Username John |
Merge Semantics (Per Key)
| Existing Value | New Value | Result |
|---|---|---|
| (absent) | any | Created |
| Plain object | Plain object | Shallow merge (new props overwrite) |
| Anything else | anything | Replacement |
Tip: To force a full replace of a nested object, first set it to null, then set the new object.
Reading Values
- Direct in app composer components:
{{ selection.rowId }}or explicit{{ local.selection.rowId }} - Via Get actions inside an action chain when an intermediate action (e.g. API Invoke) needs the value.
Reactivity With ON_DATA_HUB_CHANGE
ON_DATA_HUB_CHANGE is an element event you can attach actions to. Behind the scenes each dynamic element subscribes to the Data Hub state if (and only if) it declares this event. On any change (global/app/local) the element receives an event payload shaped like:
{
"elementContext": "<the element's own context() snapshot>",
...local,
...app,
...global,
"local": { /* local scope map */ },
"app": { /* app scope map */ },
"global": { /* global scope map */ }
}Use cases:
- Auto refresh detail panel when
selectionchanges elsewhere - Trigger conditional API prefetch when a prerequisite key appears
Best Practices:
- Scope your follow‑up actions: check for the specific key change (e.g. by storing a hash or using a Condition action) to avoid redundant work.
- Avoid chaining expensive API calls on every minor unrelated key update; consider isolating keys by namespacing.
Practical Recipes
Store a Table Selection (Local)
Row Click → Local Set (selection = JSON Interpolated: { "rowId":"{{clickedRow.id}}", "ts":"{{timestamp}}" })
Use Selection In Button API Call
Button Click chain:
- Local Get (
selection) - Check context change (
selection) - API Invoke (body includes
{{selection.rowId}}) - Local Set (
rowDetails= response)
Shared Filter Across Widgets
Input Change → App Set (activeFilter = user input). All boardlet reference activeFilter or app.activeFilter. Boardlet can also listen for On Data Hub Change
React To Data Changes
Add event ON_DATA_HUB_CHANGE to a panel element; first action is a Condition verifying selection changed; then API fetch details.
Naming Convention (Key Prefixes)
Use namespaced keys to avoid collisions as dashboards, boardlets, and apps grow.
- Use a prefix that includes at least one of: company, domain, app name (ideally 2+ for shared dashboards).
- Prefer kebab-case for the prefix and dot-notation for the rest of the key.
Recommended pattern:
<company>-<domain>-<app>-<feature>-<key>
Company prefix suggestions (choose the one your org standardizes on):
orsoftsapientqdansl(New Solution)
Domain examples:
logisticsplanningproduction
Examples:
orsoft-planning-cardCollection-selectedItemqda-logistics-dispatch-filters-activesapient-production-mes-job.-urrentId- Short, acceptable if consistent:
ors-planning.*,qdc-*
Naming & Collision Strategy
| Pattern | Guidance |
|---|---|
| Generic key reused widely | Add prefix (report.filters, tbl.selection) |
| Temporary chain scratch values | Prefix with underscore (_tempPayload) and keep Local |
Cleaning / Resetting
- Provide a Clear Local Data
On destroyevent - Navigation: optionally clear App keys via dedicated Clear action (if available) or set them to neutral defaults
- To purge multiple nested leftovers, replace parent object instead of merging
Troubleshooting Quick Table
| Symptom | Cause | Fix |
|---|---|---|
| Blank interpolation | Key not set yet | Ensure Set runs first; default {{ myKey || '—' }} |
| Stale nested data | Shallow merge kept old props | Set to null then replace OR overwrite all fields |
| Unexpected override | Same key at Local/App | Namespace or explicit global. / app. access |
Technical Reference (For Developers)
Precedence & Event Wiring
- Merge order inside context(): Global < App < Local.
DynamicElementComponentV2registers awatchStateon the injectedDataHubonly ifON_DATA_HUB_CHANGEis configured, pushing merged maps (local/app/global) plus flattened keys to action invocations.- Event enumeration:
DynamicEventTypes.ON_DATA_HUB_CHANGE(seedynamic-event-type.ts) and option with translate keyDynamicActions.Shared.Events.ON_DATA_HUB_CHANGE(seedynamic-event-options.const.ts).
Event Payload Shape (Simplified)
interface DataHubChangePayload {
elementContext: Record<string, unknown>;
// flattened merged keys (local > app > global)
// plus explicit namespaces:
local: Record<string, unknown>;
app: Record<string, unknown>;
global: Record<string, unknown>;
// other flattened keys appear at top level
}Merge Algorithm (Set Value Actions)
Pseudo:
for (const key of Object.keys(incoming)) {
const current = existing[key];
const nextVal = incoming[key];
if (current === undefined) {
existing[key] = nextVal;
} else if (isPlainObject(current) && isPlainObject(nextVal)) {
existing[key] = { ...current, ...nextVal };
} else {
existing[key] = nextVal;
}
}Performance Notes
- Shallow object merge keeps cost low; avoid nesting large mutable graphs under one key.
distinctUntilChangedwith deep equality (E1Utility.isEqual) throttlesON_DATA_HUB_CHANGEdispatches; still avoid high‑frequency churn (e.g. rapid typing) unless necessary.