Have you ever opened a Blazor solution, scrolled through a few components, and thought: “Why on earth is my state logic scattered in five different places?” If yes, you are not alone. Blazor gives you huge freedom, but with freedom comes the risk of a messy state story.
In this post I want to compare two main ways to keep Blazor state under control:
- MVVM – Model-View-ViewModel, with base components or third‑party libraries.
- MVU – Model-View-Update, the Elm‑style loop.
You will see how both look in code, where each shines in enterprise apps, and how to decide what fits your team and system.
Why state patterns matter in Blazor
Blazor has a few traps that show up only when the app grows:
- Local state in components grows into a ball of fields and
OnParametersSetAsynchacks. - Business logic leaks into
.razorfiles because “it was easier in the moment”. - Bugs come from stale state: one component updated, another did not.
A clear state pattern solves three problems:
- Traceability – you can say where and why a change happens.
- Testability – core logic can be tested without rendering the UI.
- Onboarding – new devs understand “how we do state here” in a day, not in a month.
MVVM and MVU give different answers to these needs.
MVVM vs. MVU in one minute
Very short summary:
- MVVM
- ViewModel exposes properties and commands.
- View binds to them.
- ViewModel keeps state and raises change events.
- MVU
- You keep one model per feature.
- UI sends messages.
- A pure
Updatefunction returns a new model.
Think of MVVM as objects with mutable state that the view binds to, and MVU as a pure function that takes current state + message and gives you new state.
Both fit Blazor. They just push your code into different shapes.
MVVM in Blazor
If you come from WPF, Silverlight, UWP or MAUI, MVVM feels like home. The idea is simple:
- Each component has a ViewModel.
- The ViewModel holds state and commands.
- The component binds to ViewModel properties.
You can build this with a small base class, or use a library.
Basic MVVM setup with a base component
Let’s start with a tiny home‑grown MVVM style. We will:
- Create a base
ComponentBasethat supports property change. - Create a ViewModel.
- Bind it in a
.razorfile.
Base component:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
}A simple ViewModel:
public class CounterViewModel : ViewModelBase
{
private int _count;
public int Count
{
get => _count;
set => SetField(ref _count, value);
}
public void Increment() => Count++;
}Razor component:
@page "/mvvm-counter"
@implements IDisposable
@code {
private CounterViewModel _vm = new();
protected override void OnInitialized()
{
_vm.PropertyChanged += (_, __) => StateHasChanged();
}
public void Dispose()
{
_vm.PropertyChanged -= (_, __) => StateHasChanged();
}
}
<h3>MVVM Counter</h3>
<p>Current count: @_vm.Count</p>
<button @onclick="_vm.Increment">Click me</button>Here the ViewModel owns the state. The view just binds to it and reacts to property changes.
In real code you can:
- Inject ViewModels through DI.
- Use base components that auto‑wire
PropertyChanged. - Use
INotifyPropertyChangedhelpers from libraries like CommunityToolkit.Mvvm.
Pros of MVVM in Blazor
- Familiar for XAML teams – if your team knows WPF/MAUI, they can be productive fast.
- Good for local UI logic – wizards, dialogs, complex forms with local rules.
- Fine‑grained properties – easy to bind pieces like
IsBusy,HasErrors,IsExpanded. - ViewModels live across components – shared ViewModel instance can drive several views.
Cons of MVVM in Blazor
- State spread across many objects – you have lots of ViewModels with their own small state.
- Harder to trace flows – to understand a flow, you often jump between many methods.
- Two‑way binding temptation – it is too easy to couple UI fields directly to domain models.
- More glue code –
INotifyPropertyChanged, commands, mapping between domain and ViewModel.
In small components this is fine. In big enterprise features, all the small ViewModels can be hard to keep in your head.
MVU in Blazor
MVU (Model-View-Update) comes from Elm. Many .NET devs know it from Fabulous, F# Elmish or Redux‑style libraries.
You have three core ideas:
- Model – one immutable record that holds the state for your feature.
- Messages – small types that tell you what happened (button clicked, data loaded, etc.).
- Update – a pure function
Model x Msg -> Model.
The UI reads the model and sends messages. That’s it.
A tiny MVU counter in Blazor
We can write a simple MVU component without any library.
@page "/mvu-counter"
@code {
private Model _model = new(0);
private record Model(int Count);
private enum Msg
{
Increment
}
private void Dispatch(Msg msg)
{
_model = Update(_model, msg);
}
private static Model Update(Model model, Msg msg) => msg switch
{
Msg.Increment => model with { Count = model.Count + 1 },
_ => model
};
}
<h3>MVU Counter</h3>
<p>Current count: @_model.Count</p>
<button @onclick="() => Dispatch(Msg.Increment)">Click me</button>Here:
- The model is a simple record.
- The message is an enum.
Updateis pure and returns a new model.
There is no INotifyPropertyChanged, no commands. UI change always goes through Dispatch and Update.
Where MVU feels strong
- Complex flows – checkout process, onboarding flows, multi‑step forms.
- Clear history – you can log messages and rebuild state from them.
- Easy tests –
Updatetests are just input model + message => expected model. - Predictable state – there is exactly one source of truth for that feature.
You can also add effects (HTTP calls, timers) around Update or inside messages in a disciplined way.
MVU with a small helper base class
You will often wrap the pattern in a reusable base component to reduce boilerplate.
public abstract class MvuComponent<TModel, TMsg> : ComponentBase
{
protected TModel Model { get; private set; } = default!;
protected abstract TModel InitModel();
protected abstract TModel Update(TModel model, TMsg msg);
protected override void OnInitialized()
{
Model = InitModel();
}
protected void Dispatch(TMsg msg)
{
Model = Update(Model, msg);
StateHasChanged();
}
}A feature component then focuses on the model and update logic.
@inherits MvuComponent<OrderModel, OrderMsg>
@code {
protected override OrderModel InitModel() => new(false, null, new List<OrderLine>());
protected override OrderModel Update(OrderModel model, OrderMsg msg) => msg switch
{
OrderMsg.Submit => model with { IsSubmitting = true },
OrderMsg.SubmitSuccess => model with { IsSubmitting = false, Error = null },
OrderMsg.SubmitFailed(var error) => model with { IsSubmitting = false, Error = error },
_ => model
};
private void OnSubmitClicked() => Dispatch(OrderMsg.Submit);
}
@if (Model.Error is not null)
{
<div class="alert alert-danger">@Model.Error</div>
}
<button disabled="@Model.IsSubmitting" @onclick="OnSubmitClicked">
@(Model.IsSubmitting ? "Submitting..." : "Submit order")
</button>The focus stays on the model and message flow, which scales nicely in complex features.
MVVM vs. MVU for enterprise Blazor apps
Let’s compare the two along typical enterprise concerns.
1. Team background and skills
- MVVM – great match for teams with strong XAML experience. They already think in ViewModels, commands, property change.
- MVU – easier to explain to teams with web or React/Redux background. They are used to a central store and actions.
If half your team comes from WPF and half from React, expect mixed views here.
2. State structure and ownership
- MVVM
- Many ViewModels with small pieces of state.
- State often follows the UI tree.
- Sharing state means sharing ViewModel instances.
- MVU
- One model per feature (or a small number of models).
- Clear ownership: model lives with the feature.
- Shared state handled through parent models and messages.
In big systems, MVU often makes global flows easier to reason about, while MVVM feels natural for rich control trees.
3. Testability
- MVVM – you test ViewModels. You might have to mock services and events. It is fine, but not always tiny.
- MVU – you test the pure update function. No framework, no mocks, just plain C#.
If your company has a strong testing culture, MVU gives a very clean test story.
4. Asynchronous work and side effects
Both patterns need a place for async calls.
- MVVM – you call services from commands or methods on the ViewModel. They set
IsBusy, call the API, then update properties. - MVU – you usually:
- Send a message like
LoadData. - Run a side effect (service call).
- Dispatch a result message like
LoadDataSuccessorLoadDataFailure.
The MVU style forces you to state success and failure paths as messages, which is good for clarity.
5. Tooling and third‑party support
- MVVM
- Many .NET libs support MVVM (CommunityToolkit.Mvvm, ReactiveUI, etc.).
- Tons of existing docs and samples from WPF years.
- MVU
- Fewer ready‑made libraries in C# territory, more in F#.
- Some Blazor libraries follow Redux or Elm ideas, which is close enough.
In enterprise, library choice often matters. If your company likes mature, widely known stacks, MVVM may feel safer.
6. Learning curve and cognitive load
- MVVM
- New devs must understand the link between View and ViewModel.
- In big apps, many small ViewModels and bindings can be hard to track.
- MVU
- New devs must get used to messages and pure updates.
- Once it clicks, they follow the message log to understand behaviour.
For long‑lived apps with many devs, the ability to follow message logs and read pure functions is a strong plus.
Example: the same feature in MVVM and MVU
Let’s design a small but realistic feature: a customer edit form.
We will keep it simple:
- Fields: name, email, VIP flag.
- Buttons: Save, Cancel.
- States: idle, saving, error.
MVVM version
ViewModel:
public class CustomerEditViewModel : ViewModelBase
{
private string _name = string.Empty;
private string _email = string.Empty;
private bool _isVip;
private bool _isSaving;
private string? _error;
private readonly ICustomerApi _api;
public CustomerEditViewModel(ICustomerApi api)
{
_api = api;
}
public string Name
{
get => _name;
set => SetField(ref _name, value);
}
public string Email
{
get => _email;
set => SetField(ref _email, value);
}
public bool IsVip
{
get => _isVip;
set => SetField(ref _isVip, value);
}
public bool IsSaving
{
get => _isSaving;
private set => SetField(ref _isSaving, value);
}
public string? Error
{
get => _error;
private set => SetField(ref _error, value);
}
public async Task SaveAsync(Guid id)
{
IsSaving = true;
Error = null;
try
{
await _api.UpdateAsync(id, Name, Email, IsVip);
}
catch (Exception ex)
{
Error = ex.Message;
}
finally
{
IsSaving = false;
}
}
}View:
@page "/customers/{Id:guid}/edit"
@inject CustomerEditViewModel Vm
<h3>Edit customer</h3>
@if (Vm.Error is not null)
{
<div class="alert alert-danger">@Vm.Error</div>
}
<div class="mb-3">
<label>Name</label>
<input class="form-control" @bind="Vm.Name" />
</div>
<div class="mb-3">
<label>Email</label>
<input class="form-control" @bind="Vm.Email" />
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" @bind="Vm.IsVip" />
<label class="form-check-label">VIP</label>
</div>
<button class="btn btn-primary" disabled="@Vm.IsSaving" @onclick="() => Vm.SaveAsync(Id)">
@(Vm.IsSaving ? "Saving..." : "Save")
</button>Logic lives in methods on the ViewModel. State is split into many properties.
MVU version
Model and messages:
public record CustomerEditModel(
string Name,
string Email,
bool IsVip,
bool IsSaving,
string? Error
);
public abstract record CustomerEditMsg
{
public sealed record NameChanged(string Value) : CustomerEditMsg;
public sealed record EmailChanged(string Value) : CustomerEditMsg;
public sealed record IsVipChanged(bool Value) : CustomerEditMsg;
public sealed record Save(Guid Id) : CustomerEditMsg;
public sealed record SaveSuccess : CustomerEditMsg;
public sealed record SaveFailed(string Error) : CustomerEditMsg;
}Update function:
public static class CustomerEditLogic
{
public static CustomerEditModel Init() =>
new("", "", false, false, null);
public static CustomerEditModel Update(CustomerEditModel model, CustomerEditMsg msg) => msg switch
{
CustomerEditMsg.NameChanged(var value) => model with { Name = value },
CustomerEditMsg.EmailChanged(var value) => model with { Email = value },
CustomerEditMsg.IsVipChanged(var value) => model with { IsVip = value },
CustomerEditMsg.Save => model with { IsSaving = true, Error = null },
CustomerEditMsg.SaveSuccess => model with { IsSaving = false },
CustomerEditMsg.SaveFailed(var error) => model with { IsSaving = false, Error = error },
_ => model
};
}Razor component:
@page "/customers/{Id:guid}/edit"
@inject ICustomerApi Api
@code {
private CustomerEditModel _model = CustomerEditLogic.Init();
private async Task DispatchAsync(CustomerEditMsg msg)
{
var newModel = CustomerEditLogic.Update(_model, msg);
_model = newModel;
StateHasChanged();
if (msg is CustomerEditMsg.Save(var id))
{
try
{
await Api.UpdateAsync(id, _model.Name, _model.Email, _model.IsVip);
_model = CustomerEditLogic.Update(_model, new CustomerEditMsg.SaveSuccess());
}
catch (Exception ex)
{
_model = CustomerEditLogic.Update(_model, new CustomerEditMsg.SaveFailed(ex.Message));
}
}
}
}
<h3>Edit customer (MVU)</h3>
@if (_model.Error is not null)
{
<div class="alert alert-danger">@_model.Error</div>
}
<div class="mb-3">
<label>Name</label>
<input class="form-control" value="@_model.Name"
@oninput="e => DispatchAsync(new CustomerEditMsg.NameChanged(e.Value?.ToString() ?? ""))" />
</div>
<div class="mb-3">
<label>Email</label>
<input class="form-control" value="@_model.Email"
@oninput="e => DispatchAsync(new CustomerEditMsg.EmailChanged(e.Value?.ToString() ?? ""))" />
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" checked="@_model.IsVip"
@onchange="e => DispatchAsync(new CustomerEditMsg.IsVipChanged((bool)e.Value!))" />
<label class="form-check-label">VIP</label>
</div>
<button class="btn btn-primary" disabled="@_model.IsSaving"
@onclick="() => DispatchAsync(new CustomerEditMsg.Save(Id))">
@(_model.IsSaving ? "Saving..." : "Save")
</button>In MVU you always look at the update function first to understand behaviour. In MVVM you first look at the ViewModel.
When to choose MVVM in Blazor
From experience, MVVM is a solid pick when:
- You have a strong XAML background. You already have patterns, code style and training based on ViewModels.
- Your app has many rich, local UI states. Complex grids, trees, dialogs, and controls that mostly manage their own state.
- You want to reuse existing MVVM libraries. CommunityToolkit.Mvvm, ReactiveUI, etc., give ready helpers and patterns.
- You like the object‑oriented style. You think in classes with behaviour, not in pure functions.
Typical enterprise examples:
- Back‑office admin tools with many data grids and detail panels.
- Internal tools ported from WPF.
- Highly interactive parts where ViewModels map 1:1 to complex screens.
MVVM warnings
If you go with MVVM, be strict about:
- No direct domain mutation from views. Always through ViewModels.
- Keep ViewModels thin. Push business rules to domain services.
- Avoid event spaghetti. Too many events between ViewModels will cause pain later.
When to choose MVU in Blazor
MVU shines when:
- You need strong control of flows over time. Every change to the model goes through one gate: the update function.
- You want simple tests for logic. The update function is pure and tiny to test.
- You plan heavy logging and diagnostics. You can log messages and reconstruct state for bugs.
- Your team is used to Redux‑style thinking. Front‑end devs who used React/Redux or Elm will feel at home.
Typical enterprise examples:
- Public portals with complex user flows (checkout, claims, onboarding).
- Systems that need replay of state changes for support.
- Features with many small steps and conditions.
MVU warnings
If you pick MVU, watch out for:
- Too big models. Split large features into smaller models instead of one mega record.
- Side‑effects hidden in random places. Keep async calls in a small part of the code and always reflect results as messages.
- Team pushback. For some devs the message/update style feels strange at first. Give them training and small tasks to learn it.
Can you mix MVVM and MVU in one Blazor app?
Short answer: yes.
Common mixed setup:
- Use MVU for global flows and key features.
- Use MVVM or even “plain Blazor” for small leaf components.
For example:
- The overall checkout flow uses MVU.
- A nested address editor or payment form uses a small ViewModel.
You do not have to be 100% pure. Just be clear in docs where each style is used.
Practical decision checklist
Here is a quick way to decide on a new enterprise Blazor project.
Answer each question with A or B:
- Your team is mostly:
- A: WPF/MAUI/WinForms devs.
- B: Web/React/Angular devs.
- Main pain in current systems:
- A: Complex local UI logic.
- B: Hard to trace flows and bugs over time.
- Testing culture:
- A: Some tests, not strict.
- B: Strong focus on unit and integration tests.
- Business demands:
- A: Fast delivery of forms and admin tools.
- B: Reliable tracking of flows, audits, and bug replay.
If you have mostly A answers:
- Start with MVVM.
- Wrap your base component pattern well.
- Maybe add one MVU style store for cross‑cutting state later.
If you have mostly B answers:
- Start with MVU.
- Invest in a clean message/update design.
- Allow MVVM for heavy custom controls when needed.
The key is to pick one default for your project and document it. Random mix without rules leads to chaos.
FAQ: MVVM vs. MVU in real life Blazor projects
You can, but it is not free. You will have to:
– Rewrite ViewModels into models and update functions.
– Move side‑effects into message handlers.
– Adjust tests.
If you think you may move to MVU later, start with small MVU islands first.
In practice, no serious difference for normal business apps. Blazor already re‑renders components based on diffs.
– MVU may create new model instances often, but they are small records.
– MVVM has more events and more small property updates.
Real bottlenecks usually come from DOM size, network calls, and heavy serialization, not from the pattern.
– MVVM – often in ViewModel or in separate validators called from ViewModel.
– MVU – in the update function or helpers called from it. Messages like ValidationFailed make it obvious what happened.
– One option is a global store (MVU‑style) that exposes a model and messages.
– Components can subscribe or receive parts of this state through DI.
You can still keep local MVVM or MVU per feature on top of that.
MVU fits well, because both think in terms of events/messages and state built over time. But MVVM also works if you are strict about not hiding important events inside ViewModels.
Conclusion: Picking a Blazor state pattern that will age well
You do not need a perfect pattern. You need a pattern that your team understands, that your next hire can learn fast, and that will not fall apart when the app grows.
- Choose MVVM if your team lives in ViewModels today, your app is rich in local UI details, and you want to reuse mature MVVM tooling.
- Choose MVU if you care more about clear flows, simple tests, state replay, and your team is ready to think in messages and pure updates.
- Mix both if that gives you a simple default and clear rules.
Whatever you pick, write it down in a short “state guide” for your project and stick to it.
Now the hard question: looking at your current team and app, which option fits your next Blazor feature better – MVVM, MVU, or a mix? Share your thoughts and experience; other readers (and future you) will thank you.
