ReactiveUI
Reactivity

Derived States

Building states from other states with Map and Where, combining several with RememberDerived, and MapSelf.

Map and Where

You rarely bind a state as it is, because more often you want something computed from it. Map and Where take a state and give back a new one, and the result binds, maps again and gets passed around like the original.

Try this slider, where one number drives a word, its color and the last even number you landed on:

var number = Remember(4);

var word = number.Map(x => Words[x]);
var color = number.Map(x => Color.HSVToRGB(
    x / 12f,
    0.75f,
    1f
));
var even = number.Where(x => x % 2 == 0);

Map returns a state that's always a function of its source, so several maps from one state can never disagree. The word and the color above are two views of the same number. The result can't be written to, and the only way to change it is to change the source. A mapped state is also lazy, so it recomputes only when something reads it after the source changed, then caches the result until the next change. If nobody reads it the function never runs, so you can map freely.

Where copies its source only when the value passes a check, and otherwise holds on to the last value that did. In the example, even is the last even number the slider landed on, however many odd ones it passed on the way. Until a value passes, it holds the source's initial value, whether or not that value matches. Unlike Map, it updates when the change is delivered rather than when it's read, so right after a write it still holds the old value until later in the frame. The two also chain, so number.Where(x => x > 0).Map(x => 1f / x) skips zero, as long as the source doesn't start there.

Both are extension methods on IState<T>, so they work on every kind of state, including derived states, animated states and cell contexts. Each result can also be disposed to unsubscribe from its source. You only need that when the source lives longer than the component, because otherwise they're all collected together.

Derived states

Map takes one input. For a value that depends on several, RememberDerived takes up to eight states and a function of their values, in the same order.

Below you can see the same sum built with RememberDerived and with a Map that reads b from inside:

var a = Remember(3f);
var b = Remember(4f);

var sum = RememberDerived(
    a,
    b,
    predicate: (x, y) => $"{x:0} + {y:0} = {x + y:0}"
);

// Follows a only, so moving b doesn't update it
var captured = a.Map(x => $"{x:0} + {b.Value:0} = {x + b.Value:0}");

Notice that the second line doesn't change when you move the b slider. It reads b from inside a Map, so it only follows the a slider and catches up when that one changes. Inputs have to be parameters rather than captures, because a state can only follow what it was told about.

Map has no check for this, but RememberDerived warns you when its function reads a state from outside its parameters, and this code gets that warning:

Compiles with a warning
var a = Remember(3f);
var b = Remember(4f);

var sum = RememberDerived(
    a,
    predicate: x => x + b.Value
);
Console output
App.cs(6,25): warning RV061: 'b' is a state captured by the closure, pass it as a dependency instead so its changes are tracked

The build still succeeds, so the sum just stays out of date until the a state changes. The fix is always the same: add the state to the argument list and take it as a parameter.

In the demo, sum updates when either input changes. Like Map it recomputes lazily, so two inputs changing in the same frame cost one computation with both new values, unless your code reads it between the two writes.

MapSelf

The last query is for an edge case. state.MapSelf() returns a state whose value is the state itself, and it notifies whenever the original changes. It's for reading something other than Value in a place that only receives values, most often an animated state's IsFinished or TargetValue inside RememberDerived, like in this snippet:

var shown = RememberDerived(
    anim.MapSelf(),
    color,
    predicate: (self, c) => self.IsFinished ? c : self.TargetValue
);

Without MapSelf the function would only get the animated state's current value, with no way to reach the state itself. With it, the function receives the state and still re-runs whenever the state changes.

On this page