Skip to content

Reactivity & State Reconciliation Specification ​

This specification documents the reactivity model, snapshot-based dirty detection algorithms, two-way binding propagation, and collection reconciliation semantics in RTXUI.


1. System Invariants ​

  1. Zero Runtime Wrappers: Reactive state is stored in standard C++ member variables (int, std::string, custom structs). RTXUI requires no special signal types, proxy wrappers, or accessor boilerplate.
  2. Snapshot-Driven Change Detection: Changes are detected by comparing member values against typed snapshots captured during the preceding frame reconciliation.
  3. Discrete Event-Driven Execution: State evaluation and DOM reconciliation are strictly event-driven. In the absence of terminal input or queued asynchronous tasks, the engine performs zero computation.

2. Binding Classification & Contracts ​

Members are registered via Bind() within the component constructor or InitReflection():

cpp
class Counter : public rtxui::Component<Counter> {
 public:
  int count = 0;
  std::string label = "Items";

  int double_count() const { return count * 2; }
  void Increment() { count++; }

  std::string_view view = R"html(
    <div class="panel">
      <span>{label}: {count} (Double: {double_count})</span>
      <button onclick="Increment">+1</button>
    </div>
  )html";

  Counter() {
    Bind(count);
    Bind(label);
    Bind(double_count);
    Bind(Increment);
  }
};

2.1 Member Classification Matrix ​

Member CategoryType SignatureSnapshot StorageEvaluation StageSemantic Contract
Mutable StateT member;Yes (T snapshot_)Digest()Must satisfy std::equality_comparable and std::is_copy_constructible. Evaluated with !=.
Computed PropertyT method() const;NoTemplate RenderInvoked strictly on-demand during template string expansion. Must be side-effect free.
Event Handlervoid method();NoEvent DispatchInvoked upon matching event trigger. May freely mutate bound state.
Parameterized Handlervoid method(std::string);NoEvent DispatchInvoked with string argument parsed from template attribute call syntax.
Repetition Collectionstd::vector<T>Yes (Deep copy)Digest()Element-wise equality comparison. Triggers <for> reconciliation on size or content mutation.

3. The Frame Reconciliation Lifecycle ​

When an input event or worker task executes, Screen::Step() coordinates state reconciliation via Digest():

[State Mutation in Handler / Task]
                │
                ▼
1. Snapshot Diffing (Component::Digest)
   ├── Evaluate `member != snapshot_` for each registered data member
   └── If changed: update `snapshot_ = member` and flag component as dirty
                │
                ▼
2. Template Expansion (if dirty)
   ├── Interpolate `{member}` and `{computed_method}` into XML string
   └── Parse into AST via `rtxui::xml::Parse()`
                │
                ▼
3. Virtual DOM Reconciliation
   ├── Patch existing Element hierarchy in-place
   ├── Recycle unchanged child elements
   └── Route slotted content to `<slot>` targets
                │
                ▼
4. Recursive Descendant Digest
   └── Propagate `Digest()` down active child components

Snapshot Invariant ​

Snapshots are updated atomically as each divergence is verified. If no bound members differ, Digest() returns false in $O(K)$ time (where $K$ is the number of bound properties), bypassing XML parsing, DOM traversal, and layout invalidation.


4. Two-Way Data Binding Protocol ​

Composite controls (such as <input>, <checkbox>, <radio>, <select>, and <slider>) manage internal interactive state while reflecting updates back to parent variables:

4.1 Parent-to-Child Downstream Flow ​

When a parent component passes a bound variable as an attribute:

xml
<input value="{username}" />

The evaluated string is written to the child component's matching property during reconciliation.

4.2 Child-to-Parent Upstream Flow ​

When user interaction modifies the child control:

  1. The child mutates its internal state member (e.g. checkbox::checked = true).
  2. The child dispatches PropagateBinding("checked", "true").
  3. The parent reconciler resolves the source variable bound to the attribute and assigns the new value directly to the parent's C++ member.
  4. Subsequent digest passes observe the updated value across both components in perfect synchronization.

5. Collection Keying & DOM Reconciliation ​

When rendering collections with <for each="item in items">, the reconciler maps collection items to active DOM subtrees.

5.1 Positional Reconciliation (Unkeyed) ​

xml
<for each="item in items">
  <div class="row">
    <input value="{item.name}" />
  </div>
</for>

Without an explicit key, elements are matched purely by collection index:

  • Adding or removing items at the beginning or middle causes in-place mutations across all subsequent elements.
  • Ephemeral element states (cursor position, scroll offset, running CSS transitions) remain pinned to the physical index, rather than following the logical entity.

5.2 Associative Identity Reconciliation (Keyed) ​

xml
<for each="item in items" key="item.id">
  <div class="row">
    <input value="{item.name}" />
  </div>
</for>

When key is specified:

  • Each item is assigned an identity based on the named struct field.
  • When the collection is sorted, filtered, or reordered, existing DOM nodes are repositioned rather than reconstructed.
  • Input focus, active text selection, and running CSS transitions stay anchored to the specific item.

6. Asynchronous Background State Synchronization ​

State mutations must not occur on worker threads. To update reactive state from background operations:

cpp
void FetchDataAsync() {
  std::thread([this]() {
    std::string result = BackgroundHttpCall();

    // Dispatch state update to UI loop (safe from any thread):
    rtxui::PostTask([this, result]() {
      this->status_text = result;
      // Screen event loop automatically digests and renders changes.
    });
  }).detach();
}

The UI loop executes the posted lambda, detects the mutation during the subsequent digest phase, and paints the updated frame atomically.


7. Interactive Demos ​

State Binding and Computed Values ​

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Reactive state and `{...}` interpolation.
//
// Bind(count) registers a member as reactive state: the DOM is patched whenever
// it changes. Bind() on a const method registers a computed value that is
// re-evaluated from that state, and Bind() on a plain method registers an
// `onclick` handler.
//
// Try it: click the buttons, or Tab to them and press Enter.
#include <rtxui/rtxui.hpp>

using namespace rtxui;

class Counter : public Component<Counter> {
 public:
  int count = 0;

  int double_count() const { return count * 2; }

  void Increment() { count++; }
  void Decrement() { count--; }

  std::string_view view = R"html(
      <div class="card">
        <h1>Counter</h1>

        <div class="readout">
          <div class="stat">
            <span class="label">Count</span>
            <span class="value">{count}</span>
          </div>
          <div class="stat">
            <span class="label">Doubled</span>
            <span class="value accent">{double_count}</span>
          </div>
        </div>

        <div class="actions">
          <button onclick="Decrement">-  Decrement</button>
          <button onclick="Increment">+  Increment</button>
        </div>
      </div>

      <style>
        self {
          --bg: rgb(13, 17, 23);
          --border: rgb(48, 54, 61);
          --text: rgb(230, 237, 243);
          --accent: rgb(88, 166, 255);

          display: flex;
          align-items: center;
          justify-content: center;
          width: 100%;
          height: 100%;
          background-color: var(--bg);
          color: var(--text);
        }
        .card {
          border: tall;
          border-color: var(--border);
          background-color: rgb(22, 27, 34);
          padding: 1 3;
          width: 46;
        }
        h1 {
          color: var(--accent);
          font-weight: bold;
          margin-bottom: 1;
        }
        .readout {
          display: flex;
          gap: 3;
          margin-bottom: 1;
        }
        .stat {
          display: flex;
          flex-direction: column;
          flex-grow: 1;
        }
        .label {
          color: rgb(139, 148, 158);
        }
        .value {
          font-weight: bold;
        }
        .value.accent {
          color: var(--accent);
        }
        .actions {
          display: flex;
          gap: 2;
        }
        button {
          flex-grow: 1;
          border: tall;
          border-color: var(--border);
          background-color: var(--bg);
          color: var(--text);
          padding: 0 1;
          text-align: center;
          transition: background-color 0.15s ease, border-color 0.15s ease;
        }
        button:hover, button:focus {
          border-color: var(--accent);
          color: var(--accent);
        }
        button:active {
          background-color: var(--accent);
          color: var(--bg);
        }
      </style>
    )html";

  Counter() {
    Bind(count);
    Bind(double_count);
    Bind(Increment);
    Bind(Decrement);
  }
};

int main() {
  auto app = Ref<Counter>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}

Collection Reactivity and Selection ​

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// A complete application, rather than a demo of one property.
//
// Everything here has appeared on its own elsewhere in example/ -- reactive
// state, computed values, <for> over a collection of structs, conditional
// rendering, flexbox, grid, transitions and custom properties. This file is
// about how they compose into something you would actually ship.
//
// Try it: click a service row to select it, use the filter buttons to narrow
// the list, and press Restart to watch a row transition back to healthy.
#include <rtxui/rtxui.hpp>
#include <string>
#include <vector>

using namespace rtxui;

namespace {

struct Service {
  std::string name;
  std::string region;
  std::string state;  // "healthy" | "degraded" | "down"
  int latency_ms = 0;
  int load_pct = 0;

  bool operator==(const Service& other) const = default;
};

}  // namespace

class Dashboard : public Component<Dashboard> {
 public:
  std::vector<Service> services = {
      {"api-gateway", "us-east-1", "healthy", 42, 61},
      {"auth-service", "us-east-1", "healthy", 18, 34},
      {"search-index", "eu-west-1", "degraded", 310, 88},
      {"media-encoder", "eu-west-1", "down", 0, 0},
      {"billing-worker", "us-west-2", "healthy", 27, 45},
      {"notification-bus", "ap-south-1", "degraded", 154, 72},
  };

  std::string filter = "all";
  std::string selected = "api-gateway";

  // Computed values: recomputed whenever the state above changes.
  std::string healthy_count() const { return std::to_string(Count("healthy")); }
  std::string degraded_count() const {
    return std::to_string(Count("degraded"));
  }
  std::string down_count() const { return std::to_string(Count("down")); }
  std::string total_count() const { return std::to_string(services.size()); }

  std::string selected_name() const { return selected; }
  std::string selected_region() const {
    const Service* service = Find(selected);
    return service ? service->region : "-";
  }
  std::string selected_state() const {
    const Service* service = Find(selected);
    return service ? service->state : "-";
  }
  std::string selected_latency() const {
    const Service* service = Find(selected);
    return service ? std::to_string(service->latency_ms) + " ms" : "-";
  }
  std::string selected_load() const {
    const Service* service = Find(selected);
    return service ? std::to_string(service->load_pct) : "0";
  }
  bool selected_is_down() const {
    const Service* service = Find(selected);
    return service && service->state != "healthy";
  }

  std::string filter_all_class() const {
    return filter == "all" ? "chip active" : "chip";
  }
  std::string filter_degraded_class() const {
    return filter == "degraded" ? "chip active" : "chip";
  }
  std::string filter_down_class() const {
    return filter == "down" ? "chip active" : "chip";
  }

  void ShowAll() { filter = "all"; }
  void ShowDegraded() { filter = "degraded"; }
  void ShowDown() { filter = "down"; }

  void Restart() {
    Service* service = Find(selected);
    if (service) {
      service->state = "healthy";
      service->latency_ms = 30;
      service->load_pct = 40;
    }
  }

  std::string_view view = R"html(
      <div class="app">
        <div class="header">
          <span class="brand">RTXUI</span>
          <span class="title">Service Health</span>
          <span class="spacer"></span>
          <span class="pill healthy">{healthy_count} healthy</span>
          <span class="pill degraded">{degraded_count} degraded</span>
          <span class="pill down">{down_count} down</span>
        </div>

        <div class="body">
          <div class="list">
            <div class="filters">
              <span class="{filter_all_class}" onclick="ShowAll">All</span>
              <span class="{filter_degraded_class}" onclick="ShowDegraded">Degraded</span>
              <span class="{filter_down_class}" onclick="ShowDown">Down</span>
            </div>

            <div class="rows">
              <for each="{services}" as="service">
                <div class="row {service.row_class}" onclick="Select({$index})">
                  <span class="dot {service.state}">*</span>
                  <span class="name">{service.name}</span>
                  <span class="region">{service.region}</span>
                  <span class="latency">{service.latency}</span>
                </div>
              </for>
            </div>
          </div>

          <div class="detail">
            <span class="detail-title">{selected_name}</span>
            <span class="detail-sub">{selected_region}</span>

            <div class="kv"><span class="k">State</span><span class="v">{selected_state}</span></div>
            <div class="kv"><span class="k">Latency</span><span class="v">{selected_latency}</span></div>
            <div class="kv"><span class="k">Load</span><span class="v">{selected_load}%</span></div>

            <div class="meter">
              <progress value="{selected_load}" max="100" width="26" />
            </div>

            <if condition="{selected_is_down}">
              <div class="alert">This service needs attention.</div>
              <button onclick="Restart">Restart service</button>
            </if>
          </div>
        </div>

        <div class="footer">
          <span>{total_count} services</span>
          <span class="spacer"></span>
          <span class="hint">click a row to select   .   filter above</span>
        </div>
      </div>

      <style>
        self {
          --bg: rgb(13, 17, 23);
          --surface: rgb(22, 27, 34);
          --raised: rgb(31, 38, 47);
          --border: rgb(48, 54, 61);
          --muted: rgb(139, 148, 158);
          --accent: rgb(88, 166, 255);
          --healthy: rgb(63, 185, 80);
          --degraded: rgb(210, 153, 34);
          --down: rgb(248, 81, 73);

          display: block;
          width: 100%;
          height: 100%;
          background-color: var(--bg);
          color: rgb(230, 237, 243);
        }
        .app {
          display: flex;
          flex-direction: column;
          height: 100%;
        }

        .header {
          display: flex;
          align-items: center;
          gap: 2;
          background-color: var(--surface);
          border-bottom: solid;
          border-color: var(--border);
          padding: 0 2;
        }
        .brand {
          color: var(--accent);
          font-weight: bold;
        }
        .title {
          color: var(--muted);
        }
        .spacer {
          flex-grow: 1;
        }
        .pill {
          padding: 0 1;
          font-weight: bold;
        }
        .pill.healthy { color: var(--healthy); }
        .pill.degraded { color: var(--degraded); }
        .pill.down { color: var(--down); }

        .body {
          display: flex;
          flex-grow: 1;
          gap: 1;
          padding: 1 2;
        }

        .list {
          display: flex;
          flex-direction: column;
          flex-grow: 1;
        }
        .filters {
          display: flex;
          gap: 1;
          margin-bottom: 1;
        }
        .chip {
          border: round;
          border-color: var(--border);
          color: var(--muted);
          padding: 0 1;
          transition: border-color 0.2s ease, color 0.2s ease;
        }
        .chip:hover {
          border-color: var(--accent);
          color: var(--accent);
        }
        .chip.active {
          border-color: var(--accent);
          color: var(--accent);
          font-weight: bold;
        }

        .rows {
          display: flex;
          flex-direction: column;
          border: tall;
          border-color: var(--border);
          background-color: var(--surface);
          padding: 0 1;
          flex-grow: 1;
          overflow-y: auto;
        }
        .row {
          display: flex;
          gap: 1;
          align-items: center;
          padding: 0 1;
          transition: background-color 0.2s ease;
        }
        .row:hover {
          background-color: var(--raised);
        }
        .row.selected {
          background-color: var(--raised);
          border-left: tall;
          border-color: var(--accent);
        }
        .row.hidden {
          display: none;
        }
        .dot.healthy { color: var(--healthy); }
        .dot.degraded { color: var(--degraded); }
        .dot.down { color: var(--down); }
        .name {
          width: 18;
          font-weight: bold;
        }
        .region {
          width: 12;
          color: var(--muted);
        }
        .latency {
          flex-grow: 1;
          color: var(--muted);
          text-align: right;
        }

        .detail {
          display: flex;
          flex-direction: column;
          border: tall;
          border-color: var(--border);
          background-color: var(--surface);
          padding: 1 2;
          width: 34;
        }
        .detail-title {
          color: var(--accent);
          font-weight: bold;
        }
        .detail-sub {
          color: var(--muted);
          margin-bottom: 1;
        }
        .kv {
          display: flex;
          justify-content: space-between;
          width: 100%;
        }
        .k { color: var(--muted); }
        .v { font-weight: bold; }
        .meter {
          margin-top: 1;
        }
        .alert {
          color: var(--down);
          margin-top: 1;
        }
        button {
          border: tall;
          border-color: var(--down);
          background-color: var(--bg);
          color: var(--down);
          padding: 0 1;
          margin-top: 1;
          text-align: center;
          transition: background-color 0.2s ease, color 0.2s ease;
        }
        button:hover, button:focus {
          background-color: var(--down);
          color: var(--bg);
        }

        .footer {
          display: flex;
          align-items: center;
          gap: 2;
          border-top: solid;
          border-color: var(--border);
          background-color: var(--surface);
          color: var(--muted);
          padding: 0 2;
        }
        .hint {
          color: var(--border);
        }
      </style>
    )html";

  Dashboard() {
    BindCollection("services", &services, [this](const Service& service) {
      const bool visible = filter == "all" || filter == service.state;
      std::string row_class = service.name == selected ? "selected" : "";
      if (!visible) {
        row_class += " hidden";
      }
      return std::make_shared<ManualStructVisitor>(
          std::map<std::string, std::string, std::less<>>{
              {"name", service.name},
              {"region", service.region},
              {"state", service.state},
              {"row_class", row_class},
              {"latency", service.state == "down"
                              ? std::string("--")
                              : std::to_string(service.latency_ms) + " ms"},
          });
    });

    Bind(filter);
    Bind(selected);
    Bind(healthy_count);
    Bind(degraded_count);
    Bind(down_count);
    Bind(total_count);
    Bind(selected_name);
    Bind(selected_region);
    Bind(selected_state);
    Bind(selected_latency);
    Bind(selected_load);
    Bind(selected_is_down);
    Bind(filter_all_class);
    Bind(filter_degraded_class);
    Bind(filter_down_class);
    Bind(ShowAll);
    Bind(ShowDegraded);
    Bind(ShowDown);
    Bind(Restart);

    Import("Select", [this](std::string index_str) {
      size_t index = std::stoull(index_str);
      if (index < services.size()) {
        selected = services[index].name;
      }
    });
  }

 private:
  int Count(std::string_view state) const {
    int total = 0;
    for (const Service& service : services) {
      total += service.state == state ? 1 : 0;
    }
    return total;
  }

  const Service* Find(const std::string& name) const {
    for (const Service& service : services) {
      if (service.name == name) {
        return &service;
      }
    }
    return nullptr;
  }

  Service* Find(const std::string& name) {
    return const_cast<Service*>(std::as_const(*this).Find(name));
  }
};

int main() {
  auto app = Ref<Dashboard>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}