Observable Models & Virtual Lists
Automate interactive UI state tracking without manual dirty-checking using observable models . Keep these models in lockstep
with core game data updates via synchronization dependencies (engine.addSynchronizationDependency). Prevent
DOM bloat when rendering large arrays by restricting element generation to separate pages using virtual lists .
Standard data-binding models created with engine.createJSModel are passive. When you modify their properties, the engine does not automatically
detect the change. You must call engine.updateWholeModel(modelName) to flag the model as “dirty” before the next engine.synchronizeModels() cycle
flushes the changes to the DOM.
While synchronizeModels() is typically called once per frame in your main update loop, calling updateWholeModel on a massive object (like a global
inventory or game state) forces the C++ engine to scan the entire dataset for changes. For small UI states, this is unnecessary overhead. Observable
models provide a specialized solution for active UI state.
Observable Models
Section titled “Observable Models”An observable model is a proxy object that monitors its own top-level properties. Assigning a new value to a property
automatically flags the model as dirty. You bypass the updateWholeModel call entirely, reducing JavaScript boilerplate and avoiding expensive diff
checks on large game models.
Create one using engine.createObservableModel:
engine.whenReady.then(() => { engine.createObservableModel("activeState");
// This assignment automatically flags activeState as dirty activeState.selectedHero = null;});Tracking Selection with an Index
Section titled “Tracking Selection with an Index”Consider a character selection screen. A list of heroes lives in a game model, and the UI displays the stats of the selected hero. A common pattern is to keep the active selection on the same passive model using an array index .
import Block from '@components/Layout/Block/Block';import TextBlock from '@components/Basic/TextBlock/TextBlock';
const HeroStats = () => ( <Block class="hero-stats"> <TextBlock attr:data-bind-value="{{HeroData.heroes}}[{{HeroData.selectedIndex}}].name" /> <TextBlock attr:data-bind-value="{{HeroData.heroes}}[{{HeroData.selectedIndex}}].power" /> </Block>);<div class="hero-stats"> <span data-bind-value="{{HeroData.heroes}}[{{HeroData.selectedIndex}}].name"></span> <span data-bind-value="{{HeroData.heroes}}[{{HeroData.selectedIndex}}].power"></span></div>engine.whenReady.then(() => { engine.createJSModel("HeroData", { heroes: [ { name: "Kira", role: "Scout", power: 82 }, { name: "Thane", role: "Guardian", power: 64 }, ], selectedIndex: 0, }); engine.synchronizeModels();
// Player selects the second hero HeroData.selectedIndex = 1; engine.updateWholeModel(HeroData); // Forces the engine to scan the entire HeroData object});That pattern is straightforward to wire up, but it carries two costs in a live UI:
- Binding evaluation: The SDK cannot evaluate array-index expressions (
{{HeroData.heroes}}[{{HeroData.selectedIndex}}]) directly in C++. It falls back to JavaScript evaluation, adding execution overhead to every bound element on every frame. - Model updates: Calling
updateWholeModel(HeroData)forces the engine to diff the entire array when you only changed a single index integer.
Holding a Direct Reference
Section titled “Holding a Direct Reference”Observable models fit this workflow well when you store a direct reference to the selected object in a separate lightweight model instead of tracking an index on the game data model.
import Block from '@components/Layout/Block/Block';import TextBlock from '@components/Basic/TextBlock/TextBlock';
const HeroStatsObservable = () => ( <Block class="hero-stats"> <TextBlock attr:data-bind-value="{{activeState.selectedHero.name}}" /> <TextBlock attr:data-bind-value="{{activeState.selectedHero.power}}" /> </Block>);<div class="hero-stats"> <span data-bind-value="{{activeState.selectedHero.name}}"></span> <span data-bind-value="{{activeState.selectedHero.power}}"></span></div>engine.whenReady.then(() => { engine.createJSModel("HeroData", { heroes: [ { name: "Kira", role: "Scout", power: 82 }, { name: "Thane", role: "Guardian", power: 64 }, ], });
engine.createObservableModel("activeState"); activeState.selectedHero = HeroData.heroes[0];
engine.synchronizeModels();
// Player selects the second hero activeState.selectedHero = HeroData.heroes[1];
// No updateWholeModel is needed here. // The next global synchronizeModels() call will handle it.});The binding expressions now reference properties directly ({{activeState.selectedHero.name}}). The SDK evaluates these simple property lookups
directly in C++ with zero JavaScript overhead. Furthermore, changing the selection only flags the lightweight activeState model for an update,
leaving the massive HeroData array untouched.
Keeping Referenced Data in Sync
Section titled “Keeping Referenced Data in Sync”When activeState.selectedHero points at an object inside HeroData, both models can drift apart during synchronization. Observable models only
auto-flag their own top-level property assignments. If game logic updates the hero through HeroData (for example, power increases mid-match) and you
call updateWholeModel(HeroData), bindings that read only through HeroData refresh. Bindings that read through activeState.selectedHero stay on
the previous sync cycle for activeState until something interacts with activeState and it is pushed again.
Use engine.addSynchronizationDependency(parent, dependent) to keep them aligned. When the parent model is pushed for an update, the engine pushes the dependent model in the same synchronization pass.
engine.whenReady.then(() => { // ... HeroData and activeState setup ...
// Link activeState to HeroData updates engine.addSynchronizationDependency(HeroData, activeState);
activeState.selectedHero = HeroData.heroes[1]; engine.synchronizeModels();
// Backend updates the hero's stats HeroData.heroes[1].power = 99;
// This single call now updates the UI for both HeroData AND activeState engine.updateWholeModel(HeroData); engine.synchronizeModels();});With the dependency established, the UI elements bound to activeState.selectedHero.power update instantly when the source array changes. If the
dependency is no longer needed (for example, closing the inventory menu), remove it to save processing time:
engine.removeSynchronizationDependency(HeroData, activeState);Virtual Lists
Section titled “Virtual Lists”A standard data-bind-for expression generates one DOM subtree for every element in the bound array. This is acceptable for short lists, but massive
datasets create a severe performance bottleneck. A 500-slot inventory or leaderboard generates hundreds of DOM nodes. The engine must allocate memory,
calculate layout, and track bindings for every node regardless of how many are actually visible on screen.
engine.createVirtualList() solves this DOM bloat by wrapping the array iteration in a sliding window. Instead of processing the entire array, the loop only generates elements within a defined range controlled by two properties:
startIndex: The array index where DOM generation begins (defaults to0).pageSize: The maximum number of DOM elements to generate.
To use a virtual list, you must change the standard data-bind-for syntax. Instead of iterating the raw array directly, you pass the array as an
argument to the virtual list object exposed in your model.
import Block from '@components/Layout/Block/Block';import TextBlock from '@components/Basic/TextBlock/TextBlock';
const Leaderboard = () => ( <Block class="leaderboard__list"> <Block class="leaderboard__entry" attr:data-bind-for="index, player:BoardState.page({{LeaderboardData.players}})" > <Block class="leaderboard__entry-content"> <TextBlock class="leaderboard__rank" attr:data-bind-value="{{index}} + {{BoardState.page.startIndex}} + 1" /> <TextBlock class="leaderboard__name" attr:data-bind-value="{{player.name}}" /> <TextBlock class="leaderboard__score" attr:data-bind-value="{{player.score}}" /> </Block> </Block> </Block>);<div class="leaderboard__list"> <div class="leaderboard__entry" data-bind-for="index, player:BoardState.page({{LeaderboardData.players}})"> <div class="leaderboard__entry-content"> <span class="leaderboard__rank" data-bind-value="{{index}} + {{BoardState.page.startIndex}} + 1"></span> <span class="leaderboard__name" data-bind-value="{{player.name}}"></span> <span class="leaderboard__score" data-bind-value="{{player.score}}"></span> </div> </div></div>In the HTML above, LeaderboardData.players is the standard array of game data. BoardState.page is the virtual list object. The virtual list acts
as a callable view function that intercepts the iteration and only emits the elements within its defined window.
For the HTML expression to work, you must create the virtual list in JavaScript and attach it to a model so the view layer can access it.
engine.whenReady.then(() => { // 1. Generate the raw data model engine.createJSModel("LeaderboardData", { players: [ /* ... massive array of player objects ... */ ], });
// 2. Create the virtual list and define the window constraints const virtualList = engine.createVirtualList(); virtualList.startIndex = 0; virtualList.pageSize = 5;
// 3. Expose the virtual list to the HTML via a state model engine.createJSModel("BoardState", { page: virtualList, });
engine.synchronizeModels();});When you increment virtualList.startIndex and synchronize, the engine destroys the old DOM nodes and generates the new slice. If the source array
contains 500 items and the pageSize is 5, the engine maintains a strict maximum of 5 nodes at any given time.
To increment or decrement the virtual list window, you can modify the startIndex property of the virtual list object:
const nextPage = () => { const maxStart = LeaderboardData.players.length - virtualList.pageSize; if (virtualList.startIndex + virtualList.pageSize <= maxStart) { virtualList.startIndex += virtualList.pageSize;
engine.updateWholeModel(BoardState); engine.synchronizeModels(); }};Combining Virtual Lists with Observable Models
Section titled “Combining Virtual Lists with Observable Models”Virtual lists control DOM generation volume, and observable models track interactive UI state. They are frequently used together. A common pattern is tracking which item inside a paginated virtual list the user has currently selected.
By storing the selected object in an observable model, you avoid complex index tracking across pages. The UI instantly applies an active class when the selected player is rendered in the current virtual window.
// Create the observable to track selectionengine.createObservableModel("activeState");// first player from players list is selected by defaultactiveState.selectedPlayer = LeaderboardData.players[0];
// The click handler assigns the player object, automatically dirtying the activeStateBoardState.selectPlayer = function (event, playerData) { activeState.selectedPlayer = playerData; engine.synchronizeModels();};<div class="leaderboard__entry" data-bind-for="index, player:BoardState.page({{LeaderboardData.players}})"> <div class="leaderboard__entry-content" data-bind-click="BoardState.selectPlayer(event, {{player}})" data-bind-class-toggle="is-active: {{player.name}} === {{activeState.selectedPlayer.name}}"> <span class="leaderboard__name" data-bind-value="{{player.name}}"></span> </div></div>An example of the paginated leaderboard with virtual list and observable model in action:
© 2026 Coherent Labs. All rights reserved.