Stop Prop Drilling in Blazor MAUI: When Provide Components Beat the Alternatives

Discover when to use Provide components in Blazor MAUI for cleaner state sharing. Learn the advantages, disadvantages, and how Provide compares to CascadingParameter and Fluxor for state management.

Stop Prop Drilling in Blazor MAUI: When Provide Components Beat the Alternatives

If you've worked with Vue or React, you'll recognise the pattern straight away. Provide/Inject in Vue, Context API in React - both let you pass state deep into your component tree without the tedium of prop drilling through every single level.

Blazor MAUI finally caught up with something similar: Provide components. And honestly, it's about time.

This pattern can be brilliant in the right spots, but like most state management approaches, it's not going to solve every problem. Here's when you should reach for it, when you shouldn't, and how it stacks up against alternatives like Fluxor and CascadingParameter.

What Are Provide Components?

Think of a Provide component as dependency injection for your component tree. You define a value once (data, service, configuration, whatever) and make it available to all descendant components without having to thread it through every intermediate component.

Here's the basic setup:

<!– App.razor -->
<Provide Value="@AppSettings">
    <MainLayout />
</Provide>

@code {
    AppSettings AppSettings = new() { Theme = "Dark", Language = "en" };
}

Then, somewhere deep in your component tree:

<InjectParameter Property="Settings" />

@code {
    [Parameter] public AppSettings Settings { get; set; } = default!;
}

No CascadingParameter boilerplate at every level. The Provide/InjectParameter pair handles the plumbing for you.

When This Pattern Works

I've found Provide components particularly useful for:

  • Theming and styling - Dark/light mode, font sizes, spacing preferences. These rarely change during a session and need to be accessible everywhere.
  • App-wide configuration - Feature flags, API endpoints, language settings. Again, fairly static stuff that multiple components need.
  • User context - Authenticated user info, permissions, roles. Perfect for this since user context doesn't change often but is needed all over the place.
  • Reducing prop drilling - When you're passing the same parameters through multiple levels just to get them to a deeply nested component, Provide can clean things up nicely.

The sweet spot is small to medium state sharing where the data doesn't change frequently and multiple non-adjacent components need access.

When to Look Elsewhere

Provide components aren't the right tool for:

  • Highly dynamic state - If you're building something like a chat app where messages are flying in constantly, this pattern will fight you.
  • Complex data flows - When you've got multiple write operations and intricate state dependencies, you want something purpose-built like Fluxor.
  • Large applications with multiple bounded contexts - Risk of creating spaghetti state where everything depends on everything else.

The Trade-offs

The good bits: it's lightweight (no external packages), uses native Blazor MAUI features, and the intent is clear. Perfect for stable state that rarely changes.

The not-so-good bits: changes to provided values don't automatically propagate unless you manually trigger re-renders. There's no built-in debugging tools like you get with Fluxor's time-travel debugging. And it's easy to overuse, leading to "magic data" that's hard to track down.

Provide vs Fluxor

Here's how they compare for different scenarios:

Feature Provide Components Fluxor
Setup Minimal, built-in More setup, requires package
Best for Stable context that rarely changes Complex, frequently updated app state
Performance Very fast for reads Well-optimised for reads/writes
Reactivity Manual trigger needed Automatic via state subscriptions
Debugging Limited Excellent (action logs, dev tools)
Learning curve Low Moderate

Practical Example: User Settings

Let's say you want to store user theme and language preferences. Here's how each approach looks.

With Provide Components

<!– App.razor -->
<Provide Value="@UserSettings">
    <MainLayout />
</Provide>

@code {
    UserSettings UserSettings = new() { Theme = "Dark", Language = "en" };
}
// UserSettings.cs
public class UserSettings
{
    public string Theme { get; set; } = "Light";
    public string Language { get; set; } = "en";
}
<!-- ChildComponent.razor -->
<InjectParameter Property="Settings" />

<h3>Theme: @Settings.Theme</h3>
<h3>Language: @Settings.Language</h3>

@code {
    [Parameter] public UserSettings Settings { get; set; } = default!;
}

Simple, no external packages. The downside is no automatic UI updates without manual StateHasChanged() calls.

With Fluxor

// UserSettingsState.cs
public record UserSettingsState(string Theme, string Language);

public class UserSettingsFeature : Feature<UserSettingsState>
{
    public override string GetName() => "UserSettings";
    protected override UserSettingsState GetInitialState() =>
        new("Light", "en");
}

// Actions.cs
public record SetThemeAction(string Theme);
public record SetLanguageAction(string Language);

// Reducers.cs
public static class UserSettingsReducers
{
    [ReducerMethod]
    public static UserSettingsState ReduceSetTheme(UserSettingsState state, SetThemeAction action) =>
        state with { Theme = action.Theme };

    [ReducerMethod]
    public static UserSettingsState ReduceSetLanguage(UserSettingsState state, SetLanguageAction action) =>
        state with { Language = action.Language };
}
<!-- ChildComponent.razor -->
@inject IState<UserSettingsState> UserSettings
@inject IDispatcher Dispatcher

<h3>Theme: @UserSettings.Value.Theme</h3>
<h3>Language: @UserSettings.Value.Language</h3>

<button @onclick="() => Dispatcher.Dispatch(new SetThemeAction("Dark"))">
    Switch to Dark
</button>

More boilerplate and a steeper learning curve, but you get automatic reactivity and predictable updates.

Provide vs CascadingParameter in Blazor MAUI

At first glance, Provide looks like CascadingParameter with a bow on it, but there are meaningful differences:

Feature Provide CascadingParameter
Setup Minimal - no [CascadingParameter] needed in intermediate components Must define [CascadingParameter] in every component that uses it
Intent Explicitly about injecting values down the tree More general-purpose parameter passing
Maintainability Cleaner for large trees Can get verbose with deep hierarchies
Readability Clear "this value is provided here" Less obvious, hidden in attributes
Use case App-wide values that should be globally accessible Scoped values for parent-child relationships

Rule of thumb: use Provide for global-ish values, CascadingParameter for local context.

Data Flow Comparison

The patterns work differently under the hood:

Provide Components:

App
    └── Provide(UserSettings)
         ├── Page1
         │    └── ChildComponent (gets Settings)
         └── Page2
              └── AnotherChild (gets Settings)

Fluxor:

App
  ├── Store (UserSettingsState)
  │    └── Components subscribe to state
  │    └── Dispatch actions to update
  └── Reducers handle changes

:

Bottom Line

Use Provide components for stable, infrequently changing state where you want to avoid prop drilling. Use Fluxor for frequently changing, complex state that needs proper debugging and reactivity. Use CascadingParameter for localised parent-child state sharing.

Coming from Vue's provide/inject and React's Context API, Provide in Blazor MAUI feels familiar and gets the job done. It's a clean, native way to share values down your component tree without ceremony, but it's not a replacement for proper state management.

Think of it as the right tool for specific jobs - perfect for configuration and context, but you wouldn't build an entire application's state management around it.

Subscribe to TSD

Don’t miss out on the latest posts. Sign up now to get access to the library of members-only posts.
jamie@example.com
Subscribe