# MauiReactor ## Introduction MauiReactor is a component-based UI library built on top of .NET MAUI that enables developers to build cross-platform mobile and desktop applications using pure C# with an MVU (Model-View-Update) architecture pattern. Inspired by ReactJS and Flutter, MauiReactor eliminates the need for XAML by providing a declarative, fluent API for creating user interfaces entirely in C#. The library implements a reactive programming model where UI components automatically re-render when state changes, following the same principles as modern web frameworks but adapted for native mobile development. The framework provides comprehensive support for state management, component lifecycle events, navigation, theming, animations, and hot reload capabilities for rapid development. MauiReactor wraps all native .NET MAUI controls with a declarative API and supports scaffolding custom controls and third-party libraries. It includes advanced features like dependency injection, property-based animations, Canvas-based custom drawing with SkiaSharp, and platform-specific code execution, making it a complete solution for building production-ready cross-platform applications. ## Core APIs and Components ### Component-Based Architecture with State Management Stateful component with MVU pattern implementation ```csharp class CounterState { public int Counter { get; set; } } class CounterPage : Component { public override VisualNode Render() => ContentPage("Counter Sample", VStack( Label($"Counter: {State.Counter}") .FontSize(20), Button("Click To Increment", () => SetState(s => s.Counter++)) .Margin(10) ) .Spacing(10) .Center() ); } ``` ### Props-Based Component Communication Component with properties for reusable UI elements ```csharp partial class CustomButton : Component { [Prop] string _text = string.Empty; [Prop] Color _backgroundColor = Colors.Blue; [Prop] Action? _onClicked; public override VisualNode Render() => Button(_text) .BackgroundColor(_backgroundColor) .FontSize(16) .CornerRadius(8) .Padding(16, 12) .OnClicked(_onClicked); } // Usage class MainPage : Component { public override VisualNode Render() => ContentPage("Custom Button Demo", VStack( new CustomButton() .Text("Primary Action") .BackgroundColor(Colors.Green) .OnClicked(() => Console.WriteLine("Clicked!")), new CustomButton() .Text("Secondary Action") .BackgroundColor(Colors.Gray) .OnClicked(() => Console.WriteLine("Secondary!")) ) .Spacing(12) .Center() ); } ``` ### Navigation with Props Page navigation with parameter passing ```csharp class ChildPageProps { public int InitialValue { get; set; } public Action? OnValueSet { get; set; } } class ChildPageState { public int Value { get; set; } } class ChildPage : Component { protected override void OnMounted() { SetState(s => s.Value = Props.InitialValue); base.OnMounted(); } public override VisualNode Render() => ContentPage("Child Page", VStack( Label($"Current Value: {State.Value}") .FontSize(20), Button("Increment", () => SetState(s => s.Value++)) .Margin(10), Button("Send Back", () => Props.OnValueSet?.Invoke(State.Value)) .Margin(10), Button("Go Back", async () => await Navigation.PopAsync()) .Margin(10) ) .Spacing(10) .Center() ); } class ParentPageState { public int ReceivedValue { get; set; } } class ParentPage : Component { public override VisualNode Render() => ContentPage("Parent Page", VStack( Label($"Received Value: {State.ReceivedValue}") .FontSize(20), Button("Navigate to Child", OnNavigateToChild) .Margin(10) ) .Center() ); async void OnNavigateToChild() { await Navigation.PushAsync(_ => { _.InitialValue = State.ReceivedValue; _.OnValueSet = OnValueSetFromChildPage; }); } void OnValueSetFromChildPage(int value) { SetState(s => s.ReceivedValue = value); } } ``` ### Dependency Injection Service injection into components ```csharp public interface IDataService { Task> LoadDataAsync(); } public class DataService : IDataService { public async Task> LoadDataAsync() { await Task.Delay(1000); return new List { "Item 1", "Item 2", "Item 3" }; } } partial class DataServiceComponent : Component { [Inject] IDataService? _dataService; protected override async void OnMounted() { await LoadData(); base.OnMounted(); } async Task LoadData() { SetState(s => s.IsLoading = true); var data = await _dataService.LoadDataAsync(); SetState(s => { s.Items = data; s.IsLoading = false; }); } public override VisualNode Render() => ContentPage("Data Service Demo", State.IsLoading ? VStack( ActivityIndicator() .IsRunning(true), Label("Loading...") ) .Center() : VStack( CollectionView() .ItemsSource(State.Items, item => Label(item) .Margin(10) .Padding(15) ), Button("Refresh", async () => await LoadData()) .Margin(10) ) ); } class DataServiceComponentState { public List Items { get; set; } = new(); public bool IsLoading { get; set; } } // Registration in MauiProgram.cs var builder = MauiApp.CreateBuilder(); builder .UseMauiReactorApp() .Services .AddSingleton(); ``` ### Property-Based Animation Declarative animations with state changes ```csharp class AnimatedLabelState { public double Opacity { get; set; } = 1.0; public double Scale { get; set; } = 1.0; public Color TextColor { get; set; } = Colors.Black; public double Rotation { get; set; } = 0; } class AnimatedLabel : Component { public override VisualNode Render() => ContentPage("Animation Demo", VStack( Label("Tap me to animate!") .WithAnimation(Easing.CubicInOut, 1000) .Opacity(State.Opacity) .Scale(State.Scale) .TextColor(State.TextColor) .Rotation(State.Rotation) .FontSize(24) .OnTapped(OnLabelTapped), Button("Bounce", OnBounce) .Margin(10), Button("Spin", OnSpin) .Margin(10), Button("Reset", OnReset) .Margin(10) ) .Spacing(20) .Center() ); private void OnLabelTapped() { SetState(s => { s.Opacity = s.Opacity == 1.0 ? 0.5 : 1.0; s.Scale = s.Scale == 1.0 ? 1.5 : 1.0; s.TextColor = s.TextColor == Colors.Black ? Colors.Red : Colors.Black; }); } private void OnBounce() { SetState(s => s.Scale = 2.0); SetState(s => s.Scale = 1.0, 500); } private void OnSpin() { SetState(s => s.Rotation = 360); SetState(s => s.Rotation = 0, 1000); } private void OnReset() { SetState(s => { s.Opacity = 1.0; s.Scale = 1.0; s.TextColor = Colors.Black; s.Rotation = 0; }); } } ``` ### CollectionView with Dynamic Data List rendering with ObservableCollection ```csharp class Item { public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } = DateTime.Now; } class DynamicListState { public ObservableCollection Items { get; set; } = new(); public string? SelectedItemName { get; set; } } class DynamicListPage : Component { protected override void OnMounted() { State.Items.Add(new Item { Name = "Item 1", Description = "First item" }); State.Items.Add(new Item { Name = "Item 2", Description = "Second item" }); State.Items.Add(new Item { Name = "Item 3", Description = "Third item" }); base.OnMounted(); } public override VisualNode Render() => ContentPage("Dynamic List", VStack( HStack( Button("Add Item", OnAddItem) .HFill(), Button("Clear All", OnClearAll) .HFill() ) .Margin(10), Label($"Selected: {State.SelectedItemName ?? "None"}") .FontSize(16) .Margin(10), CollectionView() .ItemsSource(State.Items, RenderItem) .EmptyView(RenderEmptyView()) ) ); private VisualNode RenderItem(Item item) => HStack( VStack( Label(item.Name) .FontSize(16) .FontAttributes(FontAttributes.Bold), Label(item.Description) .FontSize(14) .TextColor(Colors.Gray), Label(item.CreatedAt.ToString("g")) .FontSize(12) .TextColor(Colors.LightGray) ) .HFill() .OnTapped(() => SetState(s => s.SelectedItemName = item.Name)), Button("Delete") .OnClicked(() => State.Items.Remove(item)) .BackgroundColor(Colors.Red) .TextColor(Colors.White) .FontSize(12) .Padding(8) ) .Margin(10, 5) .Padding(15) .BackgroundColor(Colors.White) .CornerRadius(8); private VisualNode RenderEmptyView() => VStack( Label("No items to display") .FontSize(16) .TextColor(Colors.Gray), Label("Tap 'Add Item' to get started") .FontSize(14) .TextColor(Colors.LightGray) ) .Center() .Spacing(10); private void OnAddItem() { var newItem = new Item { Name = $"Item {State.Items.Count + 1}", Description = $"Description for item {State.Items.Count + 1}", CreatedAt = DateTime.Now }; State.Items.Add(newItem); } private void OnClearAll() { State.Items.Clear(); } } ``` ### Theming System with Dark/Light Mode Comprehensive theme definition with selectors ```csharp class AppTheme : Theme { // Color definitions public static Color Primary = Color.FromArgb("#512BD4"); public static Color PrimaryDark = Color.FromArgb("#ac99ea"); public static Color Secondary = Color.FromArgb("#DFD8F7"); public static Color White = Color.FromArgb("White"); public static Color Black = Color.FromArgb("Black"); public static Color Gray100 = Color.FromArgb("#E1E1E1"); public static Color Gray600 = Color.FromArgb("#404040"); public static Color OffBlack = Color.FromArgb("#1f1f1f"); // Theme detection private static bool LightTheme => Application.Current?.UserAppTheme == Microsoft.Maui.ApplicationModel.AppTheme.Light; public static bool IsDarkTheme => !LightTheme; // Theme selectors public const string Title = nameof(Title); public const string PrimaryButton = nameof(PrimaryButton); public const string SecondaryButton = nameof(SecondaryButton); public const string Card = nameof(Card); public static void ToggleCurrentAppTheme() { if (MauiControls.Application.Current != null) { MauiControls.Application.Current.UserAppTheme = IsDarkTheme ? Microsoft.Maui.ApplicationModel.AppTheme.Light : Microsoft.Maui.ApplicationModel.AppTheme.Dark; } } protected override void OnApply() { // Default styles LabelStyles.Default = _ => _ .TextColor(LightTheme ? Black : White) .FontSize(14); ButtonStyles.Default = _ => _ .TextColor(LightTheme ? White : Black) .BackgroundColor(LightTheme ? Primary : PrimaryDark) .FontSize(14) .CornerRadius(8) .Padding(14, 10); ContentPageStyles.Default = _ => _ .BackgroundColor(IsDarkTheme ? OffBlack : White); // Theme selectors LabelStyles.Themes[Title] = _ => _ .FontSize(24) .FontAttributes(FontAttributes.Bold) .TextColor(LightTheme ? Primary : PrimaryDark); ButtonStyles.Themes[PrimaryButton] = _ => _ .BackgroundColor(Primary) .TextColor(White) .FontSize(16) .FontAttributes(FontAttributes.Bold) .CornerRadius(8) .Padding(16, 12); ButtonStyles.Themes[SecondaryButton] = _ => _ .BackgroundColor(Secondary) .TextColor(Primary) .FontSize(16) .CornerRadius(8) .Padding(16, 12); FrameStyles.Themes[Card] = _ => _ .BackgroundColor(LightTheme ? White : Gray600) .CornerRadius(12) .Padding(16) .HasShadow(true); } } // Usage class ThemedPage : Component { public override VisualNode Render() => ContentPage("Themed App", VStack( Label("Welcome to MauiReactor") .ThemeKey(AppTheme.Title), Button("Primary Action") .ThemeKey(AppTheme.PrimaryButton) .OnClicked(() => Console.WriteLine("Primary")), Button("Secondary Action") .ThemeKey(AppTheme.SecondaryButton) .OnClicked(() => Console.WriteLine("Secondary")), Button("Toggle Theme", AppTheme.ToggleCurrentAppTheme) .Margin(20), Frame( Label("This is a themed card") ) .ThemeKey(AppTheme.Card) ) .Spacing(20) .Padding(20) ); } // Registration in MauiProgram.cs var builder = MauiApp.CreateBuilder(); builder .UseMauiReactorApp(app => { app.UseTheme(); }); ``` ### Canvas-Based Custom Drawing SkiaSharp integration for custom graphics ```csharp using MauiReactor.Canvas; class CanvasPageState { public double Progress { get; set; } = 0.5; public bool IsAnimating { get; set; } } class CanvasPage : Component { public override VisualNode Render() => ContentPage("Canvas Drawing", VStack( CanvasView( new Box { new Group { // Background circle new Ellipse() .FillColor(Colors.LightGray) .Width(200) .Height(200), // Progress arc new Ellipse() .StrokeColor(Colors.Blue) .StrokeWidth(10) .Width(200) .Height(200) .StartAngle(0) .EndAngle(360 * State.Progress), // Center text new Text($"{State.Progress * 100:F0}%") .FontSize(32) .Color(Colors.Black) .HorizontalAlignment(Microsoft.Maui.Primitives.LayoutAlignment.Center) .VerticalAlignment(Microsoft.Maui.Primitives.LayoutAlignment.Center) } } .BackgroundColor(Colors.Transparent) ) .HeightRequest(250) .Margin(20), Slider(0, 1, State.Progress) .OnValueChanged(value => SetState(s => s.Progress = value)) .Margin(20), Button("Animate Progress", OnAnimateProgress) .Margin(10), Button("Reset", () => SetState(s => s.Progress = 0)) .Margin(10) ) .Spacing(10) ); private async void OnAnimateProgress() { if (State.IsAnimating) return; SetState(s => s.IsAnimating = true); for (double i = 0; i <= 1.0; i += 0.01) { SetState(s => s.Progress = i); await Task.Delay(20); } SetState(s => s.IsAnimating = false); } } ``` ### Form Input with Validation Complex form handling with state validation ```csharp class LoginFormState { public string Email { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; public bool RememberMe { get; set; } public bool IsLoading { get; set; } public string? ErrorMessage { get; set; } public bool ShowPassword { get; set; } } class LoginForm : Component { public override VisualNode Render() => ContentPage("Login", ScrollView( VStack( Label("Welcome Back") .FontSize(32) .FontAttributes(FontAttributes.Bold) .Margin(0, 40, 0, 20), Label("Sign in to continue") .FontSize(16) .TextColor(Colors.Gray) .Margin(0, 0, 0, 40), // Email field Label("Email") .FontAttributes(FontAttributes.Bold) .Margin(0, 0, 0, 8), Entry() .Text(State.Email) .OnTextChanged(text => SetState(s => s.Email = text)) .Placeholder("Enter your email") .Keyboard(Keyboard.Email) .Margin(0, 0, 0, 16), // Password field Label("Password") .FontAttributes(FontAttributes.Bold) .Margin(0, 0, 0, 8), Grid("*", "*, 40", Entry() .Text(State.Password) .OnTextChanged(text => SetState(s => s.Password = text)) .Placeholder("Enter your password") .IsPassword(!State.ShowPassword) .GridColumn(0), Button(State.ShowPassword ? "👁️" : "👁️‍🗨️") .OnClicked(() => SetState(s => s.ShowPassword = !s.ShowPassword)) .BackgroundColor(Colors.Transparent) .GridColumn(1) ) .Margin(0, 0, 0, 16), // Remember me HStack( CheckBox() .IsChecked(State.RememberMe) .OnCheckedChanged(isChecked => SetState(s => s.RememberMe = isChecked)), Label("Remember me") .VerticalOptions(LayoutOptions.Center) ) .Margin(0, 0, 0, 24), // Error message Label(State.ErrorMessage ?? string.Empty) .TextColor(Colors.Red) .FontSize(14) .IsVisible(!string.IsNullOrEmpty(State.ErrorMessage)) .Margin(0, 0, 0, 16), // Login button Button("Sign In") .OnClicked(OnLogin) .IsEnabled(!State.IsLoading && IsFormValid()) .Margin(0, 0, 0, 16), // Loading indicator ActivityIndicator() .IsRunning(State.IsLoading) .IsVisible(State.IsLoading) .Color(Colors.Blue) .Margin(0, 0, 0, 16), // Forgot password Label("Forgot Password?") .TextColor(Colors.Blue) .OnTapped(OnForgotPassword) .Margin(0, 0, 0, 16) ) .Padding(24) ) ); private bool IsFormValid() { return !string.IsNullOrWhiteSpace(State.Email) && State.Email.Contains('@') && !string.IsNullOrWhiteSpace(State.Password) && State.Password.Length >= 6; } private async void OnLogin() { SetState(s => { s.IsLoading = true; s.ErrorMessage = null; }); try { // Simulate API call await Task.Delay(2000); if (State.Email == "test@example.com" && State.Password == "password123") { await ContainerPage.DisplayAlert("Success", "Login successful!", "OK"); // Navigate to main page } else { SetState(s => s.ErrorMessage = "Invalid email or password"); } } catch (Exception ex) { SetState(s => s.ErrorMessage = $"Error: {ex.Message}"); } finally { SetState(s => s.IsLoading = false); } } private void OnForgotPassword() { ContainerPage?.DisplayAlert("Forgot Password", "Password reset link will be sent to your email", "OK"); } } ``` ### Platform-Specific Code Conditional execution based on platform ```csharp class PlatformSpecificPage : Component { public override VisualNode Render() => ContentPage("Platform Demo", VStack( Label($"Running on: {DeviceInfo.Platform}") .FontSize(20) .Margin(10), Button("Platform Action") .OnAndroid(() => DisplayAlert("Android", "Running on Android")) .OniOS(() => DisplayAlert("iOS", "Running on iOS")) .OnWindows(() => DisplayAlert("Windows", "Running on Windows")) .OnMacCatalyst(() => DisplayAlert("Mac", "Running on macOS")) .Margin(10), Label("Platform-specific styling") .FontSize(16) .OnAndroid(label => label.TextColor(Colors.Green)) .OniOS(label => label.TextColor(Colors.Blue)) .OnWindows(label => label.TextColor(Colors.Red)) .Margin(10) ) .Center() ); private void DisplayAlert(string title, string message) { ContainerPage?.DisplayAlert(title, message, "OK"); } } ``` ### Shell Navigation with TabBar Application shell with tab-based navigation ```csharp class MainShell : Component { public override VisualNode Render() => Shell( TabBar( ShellContent("Home") .Icon("home.png") .RenderContent(() => new HomePage()), ShellContent("Profile") .Icon("profile.png") .RenderContent(() => new ProfilePage()), ShellContent("Settings") .Icon("settings.png") .RenderContent(() => new SettingsPage()) ) ); } class HomePage : Component { public override VisualNode Render() => ContentPage("Home", VStack( Label("Welcome to Home Page") .FontSize(24) .Margin(20), Button("Navigate to Detail", OnNavigateToDetail) .Margin(10) ) .Center() ); async void OnNavigateToDetail() { await CurrentShell.GoToAsync("detail"); } } class ProfilePage : Component { public override VisualNode Render() => ContentPage("Profile", VStack( Label("User Profile") .FontSize(24), Label("Name: John Doe") .Margin(10), Label("Email: john@example.com") .Margin(10) ) .Center() ); } class SettingsPage : Component { public override VisualNode Render() => ContentPage("Settings", VStack( Label("App Settings") .FontSize(24) .Margin(20), Button("Clear Cache") .Margin(10), Button("About") .Margin(10) ) .Center() ); } ``` ## Summary MauiReactor serves as a powerful alternative to traditional XAML-based .NET MAUI development, bringing modern component-based architecture and reactive programming principles to native mobile development. The framework is ideal for teams familiar with React or Flutter who want to leverage their existing knowledge while working in the .NET ecosystem. Its primary use cases include rapid prototyping of cross-platform applications, building complex data-driven UIs with real-time updates, creating custom controls with SkiaSharp-based canvas drawing, and developing maintainable enterprise applications with proper separation of concerns through components and state management. Integration with .NET MAUI is seamless—MauiReactor components can access all native MAUI controls, use platform-specific APIs, integrate with existing XAML resources, and leverage the full .NET ecosystem including dependency injection, logging, and third-party NuGet packages. The hot reload capability significantly accelerates the development workflow by allowing instant UI updates without recompiling or restarting the application. The framework's scaffolding system makes it easy to wrap third-party controls and create reusable component libraries. With comprehensive support for navigation patterns (NavigationPage, Shell, TabbedPage), theme customization, animations, and form validation, MauiReactor provides everything needed to build production-ready mobile applications with modern development practices.