Reactive values
reactive fields, computed values, effects and reacts methods - how they track what they read, when they run again, batching, and what the compiler turns them into.
On this page
Reactive code keeps derived state up to date by itself. A class declares signals (reactive fields) and values computed from them (computed); effects read them, and run again whenever something they read changes. Everything lives in System.Reactive, which the compiler uses for you; import System.Reactive; is needed only to name its types (Effect, Reactive).
| Declaration | Where | What it is |
|---|---|---|
reactive T name = value; |
A field of a class | A signal: reading it inside an effect makes the effect depend on it; assigning it runs those effects again. |
computed T Name => expression; |
A property of a class | A value computed from others, kept until something it read changes. |
effect { … } |
An expression or a statement, in any body | Runs the block now, and again whenever something it read changes. As an expression it gives the System.Reactive.Effect. |
reacts Name() { … } |
A method of a class | An effect that starts by itself once each object is constructed. |
An example
import System;
import System.Reactive;
public class Thermostat
{
public:
reactive f64 celsius = 20;
computed f64 Fahrenheit => Convert();
new()
{
effect { Console.WriteLine("constructor effect: " + celsius); }
}
reacts Watch()
{
if (celsius > 30)
Console.WriteLine("too hot");
}
Convert() : f64
{
Console.WriteLine(" (computing)");
return celsius * 9 / 5 + 32;
}
}
Main()
{
Thermostat t = new Thermostat();
Console.WriteLine(t.Fahrenheit);
Console.WriteLine(t.Fahrenheit);
str label = "F = ";
Effect shown = effect { Console.WriteLine(label + t.Fahrenheit); };
t.celsius = 35;
Reactive.Batch(() => { t.celsius = 10; t.celsius = 15; });
shown.Dispose();
t.celsius = 0;
}
larva run
constructor effect: 20
(computing)
68
68
F = 68
constructor effect: 35
too hot
(computing)
F = 95
constructor effect: 15
(computing)
F = 59
constructor effect: 0
- The constructor's effect and
Watchstart when the object is constructed; each runs once at once. Fahrenheitis computed on its first read and kept: the second read prints no(computing).- Assigning
celsius = 35runs every effect that read it: the constructor's,Watch, andshown(throughFahrenheit). - In the batch, two assignments run each effect once, with the last value.
- After
shown.Dispose(), assigningcelsiusno longer runsshown.
reactive fields
reactive T name = value; is a field of a class, read and assigned like any other. It may have an initializer and an access word, and it is per object: static reactive is error AN0031.
Reading it inside a running effect (or while a computed value is being computed) adds the effect to the field's observers. Assigning it runs each observer again, even when the new value equals the old one.
computed values
computed T Name => expression; is a read-only property whose value is the expression. It takes an expression, not accessors (AN0032).
- The first read computes the value and keeps it.
- When a reactive value it read changes, it becomes stale and notifies whoever read it, but it does not compute anything yet.
- The next read computes it again.
So a computed value costs nothing until it is read, and is computed at most once per change.
effect
effect { … } makes a System.Reactive.Effect that runs the block at once and again whenever a reactive value or computed value it read changes. Each run starts from no dependencies: it depends on what that run read, so a branch it no longer takes stops making it run.
- As a statement,
effect { … }(with no;after the block) starts an effect and does not keep it. - As an expression,
Effect e = effect { … };keeps it;e.Dispose()stops it for good. - An effect does not run again while it is running, so an effect that writes what it reads does not loop.
- An effect is a function: it uses the locals and parameters around it, and
thisin a method or constructor, as a lambda does (see Functions). - An effect started inside another effect's run is an effect of its own.
Effect also has Sources (how many sources its last run read) and Disposed.
reacts methods
reacts Name() { … } declares a method that runs as an effect of each object, started once the object is constructed. It takes no parameters, gives nothing, and has a body (AN0033). Effects of a base class start before those of the derived class.
Batches
Reactive.Batch(f) runs the function f; the effects its changes trigger run once each when it is done, instead of once per change. Batches can be nested: the effects run when the outermost one ends.
Where they go
reactive, computed and reacts declare instance members of a class or base class:
| Code | When |
|---|---|
| AN0030 | reactive, computed or reacts in a value, an interface, a record, an extension or a namespace. |
| AN0031 | static on one of them, or two of them on one member. |
| AN0032 | computed with accessors, or reactive on a property. |
| AN0033 | A reacts method with parameters, a result, or no body. |
What they become
The parser rewrites reactive members into ordinary code over System.Reactive:
| You write | It becomes |
|---|---|
reactive i32 count = 0; |
A private field $count = 0, a private Source $count_source, and a property count whose getter calls $count_source.Track() and whose setter assigns and calls $count_source.Notify(). |
computed i32 Double => count * 2; |
A private Computed<i32> $Double = new Computed<i32>(() => count * 2), and a property Double that reads its Value. |
effect { … } |
System.Reactive.Reactive.Watch(() => { … }). |
The same pieces can be used directly:
import System;
import System.Reactive;
Main()
{
Source changes = new Source();
i32 version = 0;
Effect e = Reactive.Watch(() =>
{
changes.Track();
Console.WriteLine("version " + version);
});
version = 3;
changes.Notify();
Console.WriteLine(e.Sources + " " + changes.Observers);
}
larva run
version 0
version 3
1 1
| Member | What it does |
|---|---|
Source.Track() |
Makes the running effect (if any) depend on this source. |
Source.Notify() |
Runs every effect that depends on it (at the end of the batch in progress, if any). |
Source.Observers |
How many effects depend on it. |
Reactive.Watch(f) |
Runs f now and whenever a source it tracked notifies; gives the Effect. |
Reactive.Batch(f) |
Runs f, then each triggered effect once. |
Reactive.Current |
The effect running now, or null. |
Computed<T>.Value, .Stale, .Dispose() |
The value (computed when stale), whether the next read computes it, and stopping it. |
Edit this page on GitHub Report an issue