The reactive core is Aria's foundation. Every observable value, every derived computation, every side effect flows through a single process-wide DAG (directed acyclic graph). This chapter covers the five primitives you'll use every day:
- Property<T> — observable source node
- Computed<T> — auto-tracked derived value
- Effect — auto-tracked side-effect reaction
- Subscription — RAII handle for any observation
- batch() / untracked() — transaction and escape-hatch utilities
Include: #include "aria/aria.hpp" (umbrella) or #include "aria/reactive/reactive.hpp"
Property<T>
Property<T> is the source of truth. It owns a value, notifies on change, and auto-tracks reads inside Computed / Effect.
Basic Usage
Definition property.hpp:103
Read
std::string n = name.
get();
const std::string& nr = name.
get_ref();
std::string snap = name.
peek();
const std::string& snapr = name.
peek_ref();
std::string s = name;
T peek() const noexcept(std::is_nothrow_copy_constructible_v< T >)
Snapshot read that does NOT register a dependency.
Definition property.hpp:164
const T & peek_ref() const noexcept
Snapshot read by const reference (never tracks, never copies).
Definition property.hpp:169
T get() const
Auto-tracked read.
Definition property.hpp:138
const T & get_ref() const
Auto-tracked read by const reference.
Definition property.hpp:150
Write
name = "Charlie";
items.
mutate([](std::vector<int>& v) { v.push_back(42); });
void set(const T &new_val)
Commit a new value.
Definition property.hpp:179
void mutate(Fn &&fn)
Mutate-in-place: always fires a change (cannot detect no-op because the mutation is opaque).
Definition property.hpp:189
Equality gate: set(v) where v == current is a no-op. Downstream observers are not notified. This eliminates redundant work automatically.
Observe
auto sub = name.
on_changed([](
const std::string& val) {
std::cout << "name is now: " << val << "\n";
});
auto sub2 = age.
bind([](
int val) {
std::cout << "age: " << val << "\n";
});
auto sub3 = age.
observe([](
const int& old_val,
const int& new_val) {
std::cout << "age: " << old_val << " -> " << new_val << "\n";
});
::aria::Subscription observe(std::function< void(const T &, const T &)> fn)
Two-argument form: receive (old, new).
Definition property.hpp:220
::aria::Subscription bind(std::function< void(const T &)> fn)
Fire once with the current value, then on every subsequent change.
Definition property.hpp:210
::aria::Subscription on_changed(std::function< void(const T &)> fn)
Run fn(new_value) every time the value changes.
Definition property.hpp:200
All three return a Subscription. Drop it (or call .release()) to stop receiving callbacks.
Constraints
- T must be copyable and equality-comparable (operator==)
- Property is non-copyable, non-movable — its identity in the graph is tied to its address
- Single-threaded: all reads/writes must happen on the graph thread (debug builds assert)
Computed<T>
Computed<T> derives its value from other reactive nodes. Dependencies are discovered automatically — every Property::get() or Computed::get() called inside the compute function becomes an upstream.
Basic Usage
return subtotal.
get() * (1.0 + tax_rate.
get());
}};
subtotal = 200.0;
Definition computed.hpp:86
Conditional Dependencies
Dependencies shift dynamically based on which branches execute:
return use_celsius.
get() ? celsius.
get() : fahrenheit.
get();
}};
Lazy & Memoized
- get() returns the cached value — no recomputation unless an upstream changed
- Recomputation only happens during a graph flush, in topological order
- If the recomputed value equals the previous one, downstream propagation stops (glitch-free)
Observe
Same API as Property:
auto sub2 = total.
bind([](
double val) { });
auto sub3 = total.
observe([](
double old_v,
double new_v) { });
::aria::Subscription on_changed(std::function< void(const T &)> fn)
Definition effect.hpp:160
::aria::Subscription observe(std::function< void(const T &, const T &)> fn)
Definition effect.hpp:190
::aria::Subscription bind(std::function< void(const T &)> fn)
Definition effect.hpp:179
Effect
Effect runs a side-effect function every time any tracked read changes. Equivalent to MobX autorun, SolidJS createEffect, or Svelte 5 $effect.
Basic Usage
std::cout <<
"count = " << count.
get() <<
"\n";
}};
count = 1;
count = 2;
Definition effect.hpp:127
Stop and Restart
void stop() noexcept
Explicitly cancel (without waiting for destruction).
Definition effect.hpp:141
Convert to Subscription
Effects can be transferred into a SubscriptionBag:
bag += std::move(eff).into_subscription();
Aggregate holder: owns multiple Subscriptions and drops them together.
Definition subscription.hpp:116
Subscription
Unified RAII handle for any observation — reactive graph, signals, event bus, all in one type.
RAII handle to a single subscription.
Definition subscription.hpp:44
bool active() const noexcept
Definition subscription.hpp:85
void release() noexcept
Explicitly disconnect now (instead of at destruction).
Definition subscription.hpp:83
SubscriptionBag
Aggregate holder — drop all subscriptions at once:
bag += name.
on_changed([](
const std::string&) {});
std::size_t size() const noexcept
Definition subscription.hpp:154
void clear() noexcept
Disconnect the current contents in reverse insertion order.
Definition subscription.hpp:148
In a ViewModel, track() does the same thing via the internal bag.
batch()
Coalesce multiple writes into a single flush. Observers see one notification, not N:
x = 10;
y = 20;
x = 10;
y = 20;
});
auto batch(Fn &&fn) -> decltype(fn())
Sugar: batch([&]{ firstName = "..."; lastName = "..."; }).
Definition graph.hpp:369
BatchScope (RAII)
{
x = 10;
y = 20;
}
RAII batch guard: { BatchScope b; ...; } or use batch([&]{...}).
Definition graph.hpp:346
untracked()
Opt out of auto-tracking inside a Computed or Effect:
return val + snap;
}};
auto untracked(Fn &&fn) -> decltype(fn())
Sugar: untracked([&]{ ... }).
Definition graph.hpp:329
UntrackedScope (RAII)
int snap;
{
}
return val + snap;
}};
RAII: within the scope, every dep() behaves like a plain get().
Definition graph.hpp:318
dep()
Explicit dependency hint — "treat `x` as an upstream even if I didn't read it":
std::cout << "something happened\n";
}};
requires ::aria::ReactiveNode< Reactive > void dep(Reactive &r)
Definition computed.hpp:76
Rarely needed — get() auto-tracks in 99% of cases.
CircularDependencyError
If the graph detects a cycle (A → B → A), it throws aria::CircularDependencyError instead of spinning infinitely. This is a hard error, not a warning — fix the cycle.
Quick Reference Table
| Primitive | Kind | Produces Value | Auto-Tracked | Eager First Run |
| Property<T> | Source | Yes | N/A (is source) | N/A |
| Computed<T> | Derivation | Yes | Yes | Yes |
| Effect | Reaction | No | Yes | Yes |
| batch() | Transaction | — | — | — |
| untracked() | Escape hatch | — | — | — |
| dep() | Hint | — | — | — |
See Also