Property Bindings
The generated s-properties that keep a property in sync with a state, how they're generated, and bindings on nested objects and your own components.
The s-properties
A binding connects a state to a property: for every property with a setter, there's an s-prefixed version that takes
a state instead of a value.
Here's a slider that drives a label's text, size and color through one state, and shows a hint once the level reaches 8:

var level = Remember(3);
new Label {
sText = level.Map(x => $"Level {x}"),
sFontSize = level.Map(x => 3f + x * 0.4f),
sColor = level.Map(x =>
Color.Lerp(Color.white, Color.red, x / 10f)
)
},
new Label {
Text = "Almost there!",
sEnabled = level.Map(x => x >= 8)
},
new BsSlider {
Value = level.Value,
OnValueChanged = x => level.Value = (int)x
}Assign a state and the property is set right away, then again on every change. This works for every settable property
on every component, so there's no list of bindable properties to learn. If you can write Color = x, you can write
sColor = state. One state can drive the text, size, color and visibility of several components at once, and since they
all read the same state, they can never disagree.
A control that both shows a state and changes it needs nothing extra. It reads through the s-property and reports
through its own callback, while you write the callback's value back into the state.
Below you can see a toggle and a button sharing one state, along with a line of text that follows it:

var enabled = Remember(true);
new BsToggle {
sToggled = enabled,
OnToggleStateChanged = x => enabled.Value = x
},
new BsTextButton {
Text = "Flip from code",
OnClick = () => enabled.Value = !enabled.Value
}Flip enabled from the button and the toggle moves, because it reads the state like everything else. There's only one
copy of the value, so nothing can get out of sync. Writing the value a state already has does nothing, so the toggle's
own write-back doesn't cause a second notification.
How bindings are generated
The s-properties aren't in the library. The Reactive compiler, a Roslyn source generator your project references, writes
them while you type. When it sees sText = something in a Label initializer and sText isn't a real member, it finds
the Text property on Label. It then generates an extension property that subscribes the state to it.
Because the generator works from usage, it only looks inside methods marked [StateGen]. If you try to use sText without this attribute, you'll get an error:
class App : ReactiveComponent {
protected override GameObject Construct() {
var text = Remember("Hello");
return CreateLine(text).Use();
}
private static Label CreateLine(IState<string> text) {
return new Label {
sText = text.In()
};
}
}App.cs(10,13): error CS0117: 'Label' does not contain a definition for 'sText'
The generator never looks inside this helper, so sText is never generated and Label has no such member. But if another
usage has already generated this binding, you get RV041 instead, like in this assignment outside an initializer:
class App : ReactiveComponent {
protected override GameObject Construct() {
var text = Remember("Hello");
new Label { sText = text };
return CreateLine(text).Use();
}
private static Label CreateLine(IState<string> text) {
return new Label {
sText = text.In()
};
}
}App.cs(12,13): error RV041: 'sText' is a generated binding, but this usage doesn't generate it, so it depends on another usage to exist. Assign a state to it inside an object initializer of a [StateGen] member instead.
The initializer generates sText, but the assignment below it generates nothing, so that line only works while the
initializer exists. The fix for both is to use the binding in an initializer of a [StateGen] method, so adding the
attribute makes the helper compile:
class App : ReactiveComponent {
protected override GameObject Construct() {
var text = Remember("Hello");
return CreateLine(text).Use();
}
[StateGen]
private static Label CreateLine(IState<string> text) {
return new Label {
sText = text.In()
};
}
}Construct carries the attribute and overrides inherit it, so you never have to think about it
there, but a helper method that builds part of a tree needs the attribute itself.
In Debug builds the generator also pre-generates a binding for every settable property of every type you initialize, so
completion can suggest sText before you've typed it. That's what the CompilerVisibleProperty line in the project file
is for, and Release builds generate only what's used.
Customizing the generator
You'll rarely need them, but [StateGen] takes two optional parameters that change what the generator does in a method.
Patterns
Patterns replaces the default name s{}, where {} stands for the property name, so the usual sText stops working
in that method. You can list several patterns if you want more than one naming style, like this method does:
[StateGen(Patterns = ["state_{}", "st{}"])]
protected override GameObject Construct() {
var text = Remember("Hello");
var size = Remember(6f);
return new Label {
state_Text = text,
stFontSize = size
}.Use();
}Each name binds the property it stands for, so state_Text binds Text and stFontSize binds FontSize. A pattern needs
both {} and some text around it, and the build fails without them:
class App : ReactiveComponent {
[StateGen(Patterns = ["state_{}{}"])]
protected override GameObject Construct() {
return new Label {
Text = "Hello"
}.Use();
}
}App.cs(2,27): error RV002: Pattern 'state_{}{}' is not valid. It should contain an identifier and a {} to match, e.g. 'state_{}' or '{}State'.Enabled
Enabled is on unless you say otherwise, and setting it to false makes the generator skip a method completely. It's useful
in an override, which would otherwise inherit the attribute, but that method has to set plain properties instead of bindings:
[StateGen(Enabled = false)]
protected override GameObject Construct() {
return new Label {
Text = "Hello"
}.Use();
}Subclasses inherit this too, so generation stays off for them until one adds its own [StateGen].
Binding abstractions
An s-property doesn't actually take a state. Its type is StateBinder, a small wrapper around the state, and C# creates
one through an implicit conversion when you assign a state. You never notice this with a concrete type, so anything from
Remember or Map assigns straight to the property.
The catch is that C# doesn't allow user-defined conversions from an interface type,
and parameters are usually typed as IState<T>. A parameter typed State<string> works without it since only the declared type matters,
but when using an abstract state you have to build the binder yourself. For this purpose reactive ships two extension methods: .In() and .InLazy().
While plain .In() only does a conversion, .InLazy() also skips the initial value, keeping the property's value until the first change.
Leave out .In() in this helper, which takes its state as a parameter, and the build fails:
class App : ReactiveComponent {
protected override GameObject Construct() {
var text = Remember("Hello");
return CreateLine(text).Use();
}
[StateGen]
private static Label CreateLine(IState<string> text) {
return new Label {
sText = text
};
}
}App.cs(11,21): error CS0029: Cannot implicitly convert type 'Reactive.IState<string>' to 'Reactive.Compiler.StateBinder<string, Reactive.IState<string>>'
The error names both sides of the missing conversion: the state's IState<string> type and the StateBinder that sText
expects. The state itself is fine, so this fixed version only adds .In() to build the binder directly:
class App : ReactiveComponent {
protected override GameObject Construct() {
var text = Remember("Hello");
return CreateLine(text).Use();
}
[StateGen]
private static Label CreateLine(IState<string> text) {
return new Label {
sText = text.In()
};
}
}Nested objects and your components
Bindings work at any depth of an initializer, not only on the component itself. FlexItem, FlexController and
ContentTransform are objects of their own with settable properties, so their properties get s-versions too.
Try this demo, where one slider drives a box's size, the gap between its dots and its rotation:

new Background {
FlexItem = {
sSize = amount.Map(x => new YogaVector((6f + x * 14f).pt))
},
FlexController = {
sGap = amount.Map(x => new YogaVector((x * 8f).pt))
},
ContentTransform = {
slocalEulerAngles = amount.Map(x => new Vector3(0f, 0f, x * 90f))
}
}ContentTransform is a unity component, so bindings aren't limited to Reactive's types, and any object you can
initialize gets them for its settable properties. Remember the different versions of AddCallback from
States? A generated binding subscribes with the one that fits its object. It
removes itself when a Unity object is destroyed, a component is no longer alive or a plain object like a modifier is
garbage collected. Your own components work the same way, with no attribute or interface to add, so a component with a Value property
gets sValue.
In the next example, a Meter only declares Value, and two meters bind to a state through sValue:

class Meter : ReactiveComponent {
public float Value {
get => _value.Value;
set => _value.Value = Mathf.Clamp01(value);
}
private State<float> _value = null!;
protected override GameObject Construct() {
_value = Remember(0f);
return new Background {
FlexController = {
AlignItems = Align.Stretch
},
FlexItem = {
Size = new() { x = 40.pt, y = 4.pt }
},
Children = {
new Background {
FlexItem = {
sSize = _value.Map(x =>
new YogaVector((x * 40f).pt, 100.pct)
)
}
}
}
}.Use();
}
}
// We can use sValue without explicitly defining it
new Meter { sValue = level };
new Meter { sValue = level.Map(x => 1f - x) };
new Meter { Value = 0.3f };The binding is generated into the assembly that uses it, so the property, its type and the component have to be
visible from there. A property with a private or init setter gets no binding, and neither does an internal component
from another assembly. Static properties and indexers are skipped as well.