Stateful Components

Describes what a stateful Component is and how to create it

A stateful component is a component tied to a state class that is used to keep its "state" during its lifetime.

A state is just a C# class with an empty constructor.

When a Component is first displayed on the page, i.e. the MAUI widget is added to the page visual tree, MauiReactor calls the method OnMounted().

Before the component is removed from the page visual tree MauiReactor calls the OnWillUnmount() method.

Every time a Component is "migrated" (i.e. it is preserved between a state change) the OnPropsChanged() overload is called.

OnMounted() is the ideal point to initialize the component, for example calling web services or querying the local database to get the required information to render it in the Render() method.

For example, in this code we'll show an activity indicator while the Component is loading:

public class BusyPageState
{
    public bool IsBusy { get; set; }
}

public class BusyPageComponent : Component<BusyPageState>
{
    protected override void OnMounted()
    {
        //Here is not advisable to call SetState() as the component is still not rendered yet
        State.IsBusy = true;

        //just for a test run a background task
        Task.Run(async () =>
        {
            //Simulate lengthy work
            await Task.Delay(3000);

            //finally reset state IsBusy property
            SetState(_ => _.IsBusy = false);
        });

        base.OnMounted();
    }

    public override VisualNode Render()
        => ContentPage(
            ActivityIndicator()
                .Center()
                .IsRunning(State.IsBusy)
        );
}

and this is the resulting app:

Do not use constructors to pass parameters to the component, but public properties instead (take a look at the Components Properties documentation page).

Updating the component State

When you need to update the state of your component you have to call the SetState method as shown above.

When you call SetState the component is marked as Invalid and MauiReactor triggers a refresh of the component. This happens following a series of steps in a fixed order

  1. The component is marked as Invalid

  2. The parent and ancestors up to the root component of the page are all marked as Invalid

  3. MauiReactor triggers a refresh under the UI thread that creates a new Visual tree traversing the component tree

  4. All the components that are Valid are re-used (maintained in the VisualTree) while the components marked as Invalid are discarded and a new version is created and its Render method called

  5. The new component version creates a new tree of child nodes/components that are compared with the tree linked to the old version of the component

  6. The old visual tree is compared to the new one: new nodes are created along with the native control (i.e. are mounted), removed nodes are eliminated along with the native control (i.e. are unmounted), and finally, nodes that are only changed (i.e. old and new nodes are of the same type) are migrated (i.e. native control is reused and its properties are updated according to properties of the new visual node)

  7. In the end, the native controls are added, removed, or updated

For example, let's consider what happens when we tap the Increment button in the sample component below:

class CounterPageState
{
    public int Counter { get; set; }
}

class CounterPage : Component<CounterPageState>
{
    public override VisualNode Render()
    => ContentPage("Counter Sample",
            VStack(spacing: 10,
                Label($"Counter: {State.Counter}")
                    .Center(),

                Button("Click To Increment", () =>
                    SetState(s => s.Counter++))
            )
            .Center()
        );
}

Let's now consider this revisited code:

class CounterPageState
{
    public int Counter { get; set; }
}

class CounterPage : Component<CounterPageState>
{
    public override VisualNode Render()
        => ContentPage("Counter Sample",
            VStack(spacing: 10,
                State.Counter == 0 ? new Label($"Counter: {State.Counter}")
                    .VCenter()
                    .HCenter() : null,

                Button("Click To Increment", () =>
                    SetState(s => s.Counter++))
            )
            .Center()
        );
}

When the button is clicked the variable State.Counter is updated to 1 so the component is re-rendered and the Label is umounted (i.e. removed from the visual tree) and the native control is removed from the parent VStack Control list (i.e. de-allocated):

If we click the button again, the Label component is found, again, in the new version of the Tree, so it's mounted and a new instance of the Label component is created (along with the Native control that is created and added to the parent VStack control list).

Updating the state "without" triggering a refresh

Re-creating the visual tree can be expensive, especially if the component tree is deep or the components contain many nodes; but sometimes you can update the state "without" triggering a refresh of the tree resulting in a pretty good performance improvement.

For example, consider the counter sample but with a debug message added that helps trace when the component is rendered/created (line 10):

class CounterPageState
{
    public int Counter { get; set; }
}

class CounterPage : Component<CounterPageState>
{
    public override VisualNode Render()
    {
        Debug.WriteLine("Render");
        return ContentPage("Counter Sample",
            VStack(spacing: 10,
                Label($"Counter: {State.Counter}")
                    .VCenter()
                    .HCenter(),

                Button("Click To Increment", () =>
                    SetState(s => s.Counter++))
            )
            .Center()
        );
    }
}

Each time you click the button you should see the "Render" string output in the console of your IDE: this means, as explained, that a new Visual Tree has been created.

Now, imagine for example that we just want to update the label text and nothing else. In this case, we can take full advantage of a MauiReactor feature that lets us just update the native control without requiring a complete refresh.

Let's change the sample code to this:

class CounterPageState
{
    public int Counter { get; set; }
}

class CounterPage : Component<CounterPageState>
{
    public override VisualNode Render()
    {
        Debug.WriteLine("Render");
        return ContentPage("Counter Sample",
            VStack(spacing: 10,
                Label(()=> $"Counter: {State.Counter}")
                    .Center(),

                Button("Click To Increment", () =>
                    SetState(s => s.Counter++, invalidateComponent: false))
            )
            .Center()
        );
    }
}

Notice the changes to lines 15 and 20:

15: we use an overload of the Label() the class that accepts a Func<string> 20: secondly we call SetState(..., invalidateComponent: false)

Now if you click the button, no Render message should be written to the console output: this proves that we're updating the native Label without recreating the component.

Of course, this is not possible every time (for example when a change in the state should result in a change of the component tree) but when it is, it should improve the responsiveness of the app.

Last updated