Summarize with AI:
Let’s check out the MauiReactor project and the Model-View-Update (MVU) pattern with a sample .NET MAUI app.
When creating applications using .NET MAUI, we are used to working with content pages written in XAML, view models with binding properties and data models. However, today there is an alternative that lets us forget about bindings, with a more reactive approach like those on other platforms. In this article, we will explore the project MauiReactor and the MVU (Model-View-Update) pattern, and how you can use it in your own .NET MAUI projects. Let’s get started!
MVU stands for Model-View-Update, which is a pattern that has been popularized in frameworks like React, Flutter, among others. The way each piece is defined is as follows:
The pattern results in a unidirectional and predictable flow. Unlike MVVM, in MVU there are no two-way bindings or notifications via events. Instead, we could summarize it as a state, a function that renders that state, and a controlled way to update when it will be rendered.
MauiReactor is a library hosted on GitHub, created by Adolfo Marinucci, which implements a component-based UI framework on top of .NET MAUI. This means you can write user interfaces based on the MVU pattern, using only declarative and fluent C# code.
The advantage MauiReactor offers is that Marinucci has created an equivalent for every .NET MAUI control. For example, this XAML code:
<ContentPage Title="Login">
<VerticalStackLayout VerticalOptions="Center">
<Label Text="User:" />
<Entry />
<Button Text="Login" />
</VerticalStackLayout>
</ContentPage>
Would be translated to this in MauiReactor:
class LoginPage : Component
{
public override VisualNode Render()
=> ContentPage("Login",
VStack(
Label("User:"),
Entry(),
Button("Login")
)
.Center()
);
}
For now, don’t worry about the implementation; rather, I want you to notice how the interface is defined through the use of fluent methods, which makes the code readable. Also, there’s no XAML code or bindings.
MauiReactor is distributed via the NuGet package Reactor.Maui, although it’s also possible to install a preconfigured template as indicated by the official repository. To do this, you should run the following command in the terminal:
dotnet new install Reactor.Maui.TemplatePack
Once the template has been created, we can create a new project using the following command:
dotnet new maui-reactor-startup -o MvuHabitTracker
One of the important steps for MauiReactor to work in a .NET MAUI project is that the traditional builder.UseMauiApp<App>() must be replaced by builder.UseMauiReactorApp<MainPage>(). This tells MauiReactor that the component MainPage will be the first screen shown.
Now that the project has been configured, let’s see how to create a component in MauiReactor. Let’s create a simple one that shows a message to the user, without any state. To do this, I will create a new class called MainPage.cs with the following content:
using MauiReactor;
namespace MvuHabitTracker.Components;
//1.
class MainPage : Component
{
//2.
public override VisualNode Render()
//3.
=> ContentPage("Habit Tracker",
//4.
Label("Here we'll build our app with the MVU pattern!")
.FontSize(18)
.Center()
);
}
Let’s take a closer look at the code:
MainPage inherits from Component, which is the base class for every stateless component in MauiReactor.Render is overridden, which returns the visual tree of the screen.Label as content.When running the application, we will get a result like the following:

Now, let’s go a step further and implement state to react to user actions.
To be able to add state to a component in MauiReactor, we must do two things. First, define a class that represents the state, and, second, make the component inherit from Component<TState>. Let’s go back to our file MainPage.cs and modify it as follows:
namespace MvuHabitTracker
{
//1.
class CounterState
{
public int Counter { get; set; }
}
//2.
class MainPage : Component<CounterState>
{
public override VisualNode Render()
=> ContentPage("Counter Sample",
VStack(spacing: 10,
//3.
Label($"Counter: {State.Counter}")
.FontSize(20)
.Center(),
//4.
Button("Click To Increment", () =>
//5.
SetState(s => s.Counter++))
)
.Center()
);
}
}
Once again let’s analyze the class in detail:
CounterState, with a single property Counter. This class will represent the state of the screen and is the Model part of the pattern.MainPage, adding a generic of CounterState. This allows us to access the property called State, which gives us access to the component’s state.State is used where we want the component’s information to be displayed. In our example we use an interpolated string to show the state of Counter in the Label. Here we see the View part of the MVU pattern.SetState(s => s.Counter++)) is invoked, representing the Update part of the pattern.We have seen the different parts of the MVU pattern in action in the previous code. Now, what really happens behind the scenes?
Internally, when invoking SetState, the component is marked as invalid. Then the method Render is re-executed, which generates a new visual tree with the updated state. Finally, the new tree is compared with the previous one and only the differences are applied to the native controls. The result of the execution is the following:

Now that we have the basics of how MauiReactor works, let’s create a more useful application.
We are going to create a simple application that allows us to display a series of habits with a name, a daily goal and a progress. To do this, let’s start by creating a new model called Habits:
public class Habit
{
public string Name { get; set; } = string.Empty;
public int Count { get; set; }
public int Goal { get; set; } = 1;
public bool IsCompleted => Count >= Goal;
}
Something interesting in the previous model is that the property IsCompleted is self-calculated based on whether the goal was reached or not, returning a true value when it is true.
Now, let’s update MainPage as follows:
//1.
class MainPageState
{
public List<Habit> Habits { get; set; } = new();
}
class MainPage : Component<MainPageState>
{
//2.
protected override void OnMounted()
{
State.Habits = new List<Habit>
{
new() { Name = "Drink water",Goal = 8 },
new() { Name = "Read", Goal = 3 },
new() { Name = "Exercise", Goal = 1 },
};
base.OnMounted();
}
public override VisualNode Render()
=> ContentPage("Habit Tracker",
//3.
ScrollView(
//4.
VStack(spacing: 12,
Label("Today's Habits")
.FontSize(26)
.FontAttributes(FontAttributes.Bold)
.HCenter()
)
.Padding(20)
)
);
}
Several interesting things happen in the previous code:
MainPageState a list-type property of habits initialized as empty.OnMounted is a lifecycle method of the component that MauiReactor invokes only once, when it is shown for the first time. Because of this, we use it to initialize the list with data.ScrollView so the list of habits can scroll if it grows.The previous code only defines a Label with the main title of the page, but it still does not display the list information. To achieve this, we are going to create a new smaller reusable component.
Although we could have created the whole UI from the main component, the ideal is always to have small blocks that we can reuse throughout the app. In this case, we will create a card-like component that receives a habit and shows a card with its name, progress and a button to increment it.
We will achieve this by creating a file HabitCard.cs:
//1.
partial class HabitCard : Component
{
//2.
[Prop]
Habit _habit = new();
[Prop]
//4.
Action _onIncrement = () => { };
public override VisualNode Render()
=> Border(
Grid("Auto", "*,Auto",
VStack(spacing: 2,
Label(_habit.Name)
.FontSize(16)
.FontAttributes(FontAttributes.Bold),
Label($"{_habit.Count} / {_habit.Goal}")
.FontSize(13)
.TextColor(Colors.Gray)
)
.GridColumn(0)
.VCenter()
.Padding(12, 0),
//4.
Button(_habit.IsCompleted ? "?" : "+", _onIncrement)
.GridColumn(1)
.WidthRequest(52)
.VCenter()
)
)
.Padding(14)
//4.
.Stroke(_habit.IsCompleted ? Colors.MediumSeaGreen : Colors.LightGray)
.StrokeThickness(1)
.StrokeShape(new RoundRectangle().CornerRadius(12));
}
The previous code shows some concepts that we have not yet explained, so I will take the opportunity to point them out to you:
partial, because MauiReactor uses a code generator to create fluent methods from our properties.[Prop] attributes, which will allow the parent component to configure the properties.Action type property is used, which will allow the child component to notify the parent that a button was pressed, without the parent knowing what should happen next.With this new component ready, let’s return to the main page to integrate it.
The time has come to integrate the new child component into the main component. To achieve this, we will complete the Render method, creating a loop to iterate over each habit and display it through the new component:
...
using System.Linq;
...
class MainPage : Component<MainPageState>
{
public override VisualNode Render()
{
//1.
int completed = State.Habits.Count(habit => habit.IsCompleted);
//2.
var children = new List<VisualNode>
{
Label("Today's Habits")
.FontSize(26)
.FontAttributes(FontAttributes.Bold)
.HCenter(),
Label($"{completed} of {State.Habits.Count} completed")
.FontSize(15)
.TextColor(Colors.Gray)
.HCenter()
.Margin(0, 0, 0, 8),
};
//3.
foreach (var habit in State.Habits)
{
children.Add(new HabitCard()
.Habit(habit)
//4.
.OnIncrement(() => SetState(_ => habit.Count++)));
}
children.Add(
//5.
Button("Reset day", () =>
SetState(_ =>
{
foreach (var habit in State.Habits)
{
habit.Count = 0;
}
}))
.Margin(0, 12, 0, 0));
//6.
return ContentPage("Habit Tracker",
ScrollView(
VStack(spacing: 12, children.ToArray())
.Padding(20)
)
);
}
}
In the previous code we do the following:
children of VisualNode nodes is created, which are visual nodes of MauiReactor. Each item will have a title and the progress of completed tasks.HabitCard. Since we use the [Prop] attribute in the subcomponent’s properties, we can configure what will happen when its button is pressed.OnIncrement we call the SetState method, which triggers a new render. Both the subtitle and the card will update automatically.Something we have seen in the implementation is that at no time are the controls manipulated directly. Rather, as the state changes, the entire interface is regenerated and MauiReactor applies only the differences.
When running the application, we will have the following result:
![]()
We can see that the result is a fully functional app, with an architecture completely different from what we are used to.
Throughout this article, we have explored what MauiReactor and the MVU pattern are, applied to .NET MAUI projects. We have seen that the pattern proposes an architecture different from the traditional MVVM, in which there are no bindings or notifications.
It is certainly an interesting proposal, although we must remember that it does not have official support from the .NET MAUI team. I invite you to explore the repository and experiment with your own projects.
Héctor Pérez is a Microsoft MVP with more than 10 years of experience in software development. He is an independent consultant, working with business and government clients to achieve their goals. Additionally, he is an author of books and an instructor at El Camino Dev and Devs School.