ReactiveUI
Reactivity

States

What a state is, how it's read and written, callbacks and their lifetime, and how changes reach readers.

States

A state is a value that tells everyone reading it when it changes, and everything reactive in the library is built on this one idea.

Here's the smallest example, a number and a button that adds to it. The button only changes count, and the label keeps up on its own:

Counter.cs
class Counter : ReactiveComponent {
    protected override GameObject Construct() {
        var count = Remember(0);

        return new Layout {
            FlexController = {
                FlexDirection = FlexDirection.Column,
                AlignItems = Align.Center,
                JustifyContent = Justify.Center,
                Gap = 2.pt
            },

            Children = {
                new Label {
                    FontSize = 9f,
                    sText = count.Map(x => x.ToString())
                },

                new BsTextButton {
                    Text = "+1",
                    OnClick = () => count.Value++
                }
            }
        }.Use();
    }
}

Remember creates a state with a starting value, here 0. Reading it always gives you the latest value, even if the readers haven't heard about the change yet. Writing to it lets the readers know, unless the new value equals the current one, in which case nothing happens. "Equal" is whatever EqualityComparer<T>.Default says, which by default means the same object for a class and the same contents for a record.

Views read a state through a binding, an s-prefixed property like sText that stays in sync with it. Property Bindings covers those in detail. To run your own code when a state changes, use a callback, as described below.

If you change an object without assigning a new one, the state can't tell and readers won't hear about it. Call EnforceInvalidation() to let them know anyway.

Callbacks

A callback runs code when a state changes, and there are two ways to add one:

count.AddCallback(x => Debug.Log(x));
count.AddCallback(x => Debug.Log(x), lazy: true);

The first runs once right away with the current value, then again on every change. That's usually what you want, since whatever the callback does to apply a value gets done for the starting value too. lazy: true skips that first call, so use it for callbacks that are about changes rather than values, like a log, a sound or an animation trigger.

Below you can see one of each added to the same state with their runs counted, so the eager count starts at 1 and the lazy one at 0:

Callbacks.cs
class Callbacks : ReactiveComponent {
    protected override GameObject Construct() {
        var value = Remember(0);
        var eagerRuns = Remember(0);
        var lazyRuns = Remember(0);

        value.AddCallback(_ => eagerRuns.Value++);
        value.AddCallback(_ => lazyRuns.Value++, lazy: true);

        return new Layout {
            FlexController = {
                FlexDirection = FlexDirection.Column,
                AlignItems = Align.Center,
                JustifyContent = Justify.Center,
                Gap = 2.pt
            },

            Children = {
                new Label { sText = eagerRuns.Map(x => $"eager runs: {x}") },
                new Label { sText = lazyRuns.Map(x => $"lazy runs: {x}") },

                new BsTextButton {
                    Text = "Change the value",
                    OnClick = () => value.Value++
                }
            }
        }.Use();
    }
}

AddCallback returns a StateSubscription you can call RemoveCallback() on to stop listening, and until you do, the plain form above keeps running. That's fine when the state and the subscriber go away together, as they do for a state a component created for its own tree, but becomes a leak when the state outlives the subscriber. A settings state in a service, read by a screen that opens and closes many times, keeps every closed screen's callbacks alive, and the screens along with them.

For a shared state, use the overloads that take the subscriber:

settings.Volume.AddCallback(this, (self, x) => self._slider.Value = x);
settings.Theme.AddCallbackUnity(image, (img, x) => img.color = x);
settings.Width.AddCallbackWeak(modifier, (mod, x) => mod.Size = x);

All three remove the subscription for you, the next time the state notifies after the target is gone. They differ in how they tell that it's gone:

  • AddCallback(target, ...) takes any ILifetimeProvider, which every component is, and checks IsAlive.
  • AddCallbackUnity takes a Unity object and checks that it hasn't been destroyed.
  • AddCallbackWeak holds the target through a WeakReference and checks that it hasn't been garbage collected, for objects with no lifetime of their own, like a layout modifier.

In all three the lambda receives the target as a parameter instead of capturing it. A captured this would keep the component alive through the closure, and the liveness check could never fail. Generated bindings use these overloads and pick the right one for the property's owner, so a bound label never leaks.

How changes are delivered

Setting a state does two things at two different times, and most questions about states come down to this.

First, right away, the state marks everything derived from it as stale, meaning anything built with Map, Where or RememberDerived. No functions run at this point, because a derived state simply recomputes the next time it's read. Second, later in the frame, the state delivers the change. Every changed state with callbacks waits in a queue, which runs once per frame, after LateUpdate and right before the UI is drawn. Each callback then runs once with the state's latest value. Bindings are callbacks, so this is also when properties are set and the screen changes.

This is what makes states cheap. Ten writes to a state in one frame become one delivery with the final value, so a state an animation writes every frame costs one callback per frame. A loop that updates a state a thousand times costs the same as a single assignment. A derived state whose inputs both changed is computed once with both new values, unless your code reads it between the two writes. Callbacks run after both writes have landed, so they never see one input updated and the other not. A change that reaches a state along two paths is also delivered only once. A state nobody listens to gets no deliveries at all, so reading it only through Value has no per-frame cost.

Try this demo, which writes a state 1000 times per click while its delivery count only goes up by one each time:

Batching.cs
class Batching : ReactiveComponent {
    protected override GameObject Construct() {
        var count = Remember(0);
        var deliveries = Remember(0);

        count.AddCallback(_ => deliveries.Value++, lazy: true);

        return new Layout {
            FlexController = {
                FlexDirection = FlexDirection.Column,
                AlignItems = Align.Center,
                JustifyContent = Justify.Center,
                Gap = 2.pt
            },

            Children = {
                new Label { sText = count.Map(x => x.ToString()) },
                new Label { sText = deliveries.Map(x => $"{x} deliveries") },

                new BsTextButton {
                    Text = "Write 1000 times",
                    OnClick = () => {
                        for (var i = 0; i < 1000; i++) count.Value++;
                    }
                }
            }
        }.Use();
    }
}

Notice that the label jumps straight from 0 to 1000. The 999 values in between are never delivered, so no callback or binding sees those intermediate steps and they never reach the screen.

Callbacks can write states too, and those changes are delivered in the same pass. A cascade settles within the frame, so the screen never shows an intermediate step.

On this page