<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Redux Architecture for Blazor: Predictable State for Applications That Grow]]></title><description><![CDATA[Redux Architecture for Blazor: Predictable State for Applications That Grow]]></description><link>https://strongerbyai.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 13 Nov 2026 18:57:25 GMT</lastBuildDate><atom:link href="https://strongerbyai.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Redux Architecture for Blazor: Predictable State for Applications That Grow]]></title><description><![CDATA[Apply Redux (Flux) to Blazor with Fluxor: actions, reducers, effects, testing, and how to keep AI agents in check with a human in the loop.]]></description><link>https://strongerbyai.hashnode.dev/redux-architecture-for-blazor-predictable-state-for-applications-that-grow</link><guid isPermaLink="true">https://strongerbyai.hashnode.dev/redux-architecture-for-blazor-predictable-state-for-applications-that-grow</guid><category><![CDATA[Blazor]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[Redux]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[CEDRIC DERUE]]></dc:creator><pubDate>Thu, 12 Nov 2026 15:53:57 GMT</pubDate><content:encoded><![CDATA[<p>Blazor delivers on its promise: rich web applications written in C#, with a component model close to what JavaScript developers know. But as with any component framework, the question of state eventually arrives. While an application has three pages, cascading parameters and a few scoped services are enough. Once you have dozens of components, screens that share data, concurrent API calls and several teams working in parallel, state becomes the first source of bugs and technical debt.</p>
<p>This article presents a Redux-inspired architecture (more precisely, the Flux pattern) adapted to Blazor, using the Fluxor library as the reference implementation. The goal is not to add one more tutorial, but to lay out the architectural choices, their trade-offs and the traps to avoid. A final section covers a recent and particularly relevant use of the pattern: framing AI agents that act on your application’s state.</p>
<h2>1. The problem: scattered state</h2>
<p>In a “classic” Blazor application, state lives in several places:</p>
<ul>
<li>fields and properties of components (local state);</li>
<li><code>[Parameter]</code> and <code>CascadingValue</code> (state passed from parent to child);</li>
<li>scoped or singleton services, often with a home-made <code>OnChange</code> event;</li>
<li>browser storage (<code>localStorage</code>, <code>sessionStorage</code>);</li>
<li>the URL (route, query string).</li>
</ul>
<p>The symptoms are well known:</p>
<ul>
<li><strong>Scattered mutations</strong>: any component can modify a shared service, so “who changed this value, and why?” becomes impossible to answer.</li>
<li><strong>Inconsistent refreshes</strong>: you have to remember to call <code>StateHasChanged()</code> in the right place, and each service invents its own notification mechanism.</li>
<li><strong>Async logic mixed with UI</strong>: API calls, <code>try/catch</code> blocks and loading flags pile up inside components.</li>
<li><strong>Hard-to-test code</strong>: presentation logic is buried in components that are difficult to isolate.</li>
</ul>
<p>Redux is not the only answer, but it is a proven one, simple in its principles and remarkably readable at scale.</p>
<h2>2. The principles of Redux</h2>
<p>Redux rests on three rules, inherited from the Flux architecture:</p>
<ol>
<li><strong>A single source of truth</strong>: the application state is one tree (the <em>store</em>), split into <em>features</em>.</li>
<li><strong>State is read-only</strong>: you never modify it directly. The only way to change it is to emit an <em>action</em>, an object describing what happened.</li>
<li><strong>Changes go through pure functions</strong>: <em>reducers</em> take the current state and an action, and return a new state, with no side effects.</li>
</ol>
<p>The flow is strictly unidirectional:</p>
<p><strong>Component → Action → Reducer → New state → Component</strong></p>
<p>Side effects (HTTP calls, storage, navigation) are isolated in a dedicated layer, the <em>effects</em>, which receive actions and emit other actions in return.</p>
<h2>3. Why this model fits C# and Blazor</h2>
<p>The pattern adapts naturally to the .NET ecosystem, for several reasons:</p>
<ul>
<li><strong>C# records</strong> provide immutability and non-destructive copying with <code>with</code>, exactly what reducers need.</li>
<li><strong>Strong typing</strong> makes actions explicit: no magic strings, and the compiler checks consistency between emitters and consumers.</li>
<li><strong>Blazor’s rendering model</strong> relies on change detection and <code>StateHasChanged()</code>: a store that notifies subscribers when state changes plugs in cleanly.</li>
<li><strong>Dependency injection</strong> lets you supply effects with their services (HttpClient, API clients, logging) without coupling them to components.</li>
</ul>
<h2>4. Architecture overview</h2>
<p>The building blocks of a Redux architecture for Blazor:</p>
<table>
<thead>
<tr>
<th>Building block</th>
<th>Role</th>
<th>Fluxor equivalent</th>
</tr>
</thead>
<tbody><tr>
<td><strong>State</strong></td>
<td>Immutable data of a feature</td>
<td><code>[FeatureState]</code> on a <code>record</code></td>
</tr>
<tr>
<td><strong>Action</strong></td>
<td>Describes an event (intent or result)</td>
<td>Plain <code>record</code></td>
</tr>
<tr>
<td><strong>Reducer</strong></td>
<td>Pure function: (state, action) → new state</td>
<td><code>[ReducerMethod]</code></td>
</tr>
<tr>
<td><strong>Effect</strong></td>
<td>Handles asynchronous side effects</td>
<td><code>[EffectMethod]</code></td>
</tr>
<tr>
<td><strong>Dispatcher</strong></td>
<td>Single entry point to emit actions</td>
<td><code>IDispatcher</code></td>
</tr>
<tr>
<td><strong>Store</strong></td>
<td>Container that orchestrates everything</td>
<td><code>IStore</code></td>
</tr>
<tr>
<td><strong>Selector / State</strong></td>
<td>How components read state</td>
<td><code>IState&lt;T&gt;</code></td>
</tr>
</tbody></table>
<p>A simple organizing rule: <strong>one folder per feature</strong>, containing its state, actions, reducers and effects. This avoids giant technical folders (“Actions”, “Reducers”) that scatter the logic of a single feature.</p>
<pre><code class="language-plaintext">/eShop
  /Catalog
    CatalogState.cs
    CatalogActions.cs
    CatalogReducers.cs
    CatalogEffects.cs
  /Cart
    CartState.cs
    ...
</code></pre>
<h2>5. Implementation with Fluxor</h2>
<p><a href="https://github.com/mrpmorris/Fluxor">Fluxor</a> is the most widely used Redux/Flux implementation for Blazor. It works in Blazor Server and WebAssembly alike.</p>
<h3>5.1 Installation and configuration</h3>
<p>After adding the <code>Fluxor.Blazor.Web</code> package, register Fluxor in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">builder.Services.AddFluxor(options =&gt;
{
    options.ScanAssemblies(typeof(Program).Assembly);
#if DEBUG
    options.UseReduxDevTools();
#endif
});
</code></pre>
<p>Then initialize the store in the root component (<code>App.razor</code> or <code>Routes.razor</code>):</p>
<pre><code class="language-razor">&lt;Fluxor.Blazor.Web.StoreInitializer /&gt;
</code></pre>
<h3>5.2 Defining state</h3>
<p>State is an immutable <code>record</code>. It holds everything the UI needs, including loading and error indicators.</p>
<pre><code class="language-csharp">[FeatureState]
public sealed record CatalogState
{
    public bool IsLoading { get; init; }
    public string? Error { get; init; }
    public IReadOnlyList&lt;Product&gt; Products { get; init; } = [];

    // Parameterless constructor required by Fluxor
    private CatalogState() { }

    public CatalogState(bool isLoading, string? error, IReadOnlyList&lt;Product&gt; products)
        =&gt; (IsLoading, Error, Products) = (isLoading, error, products);
}
</code></pre>
<h3>5.3 Defining actions</h3>
<p>Actions are simple records, named as events. A useful convention is to separate the <strong>intent</strong> (“load the catalog”) from the <strong>outcomes</strong> (“loaded successfully”, “failed”).</p>
<pre><code class="language-csharp">public record LoadCatalogAction;
public record LoadCatalogSuccessAction(IReadOnlyList&lt;Product&gt; Products);
public record LoadCatalogFailureAction(string Error);
</code></pre>
<h3>5.4 Writing reducers</h3>
<p>A reducer is a pure static method. No network call, no clock, no random ID generation: same input, same output.</p>
<pre><code class="language-csharp">public static class CatalogReducers
{
    [ReducerMethod]
    public static CatalogState OnLoad(CatalogState state, LoadCatalogAction _)
        =&gt; state with { IsLoading = true, Error = null };

    [ReducerMethod]
    public static CatalogState OnSuccess(CatalogState state, LoadCatalogSuccessAction action)
        =&gt; state with { IsLoading = false, Products = action.Products };

    [ReducerMethod]
    public static CatalogState OnFailure(CatalogState state, LoadCatalogFailureAction action)
        =&gt; state with { IsLoading = false, Error = action.Error };
}
</code></pre>
<h3>5.5 Handling side effects</h3>
<p>Effects are the only place where you talk to the outside world. They are instantiated through dependency injection.</p>
<pre><code class="language-csharp">public class CatalogEffects(ICatalogApi api)
{
    [EffectMethod]
    public async Task OnLoad(LoadCatalogAction _, IDispatcher dispatcher)
    {
        try
        {
            var products = await api.GetProductsAsync();
            dispatcher.Dispatch(new LoadCatalogSuccessAction(products));
        }
        catch (Exception ex)
        {
            dispatcher.Dispatch(new LoadCatalogFailureAction(ex.Message));
        }
    }
}
</code></pre>
<h3>5.6 Consuming state in a component</h3>
<p>The component inherits from <code>FluxorComponent</code>, which subscribes to state, triggers a render on every change and unsubscribes cleanly on disposal.</p>
<pre><code class="language-razor">@page "/catalog"
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject IState&lt;CatalogState&gt; State
@inject IDispatcher Dispatcher

@if (State.Value.IsLoading)
{
    &lt;p&gt;Loading…&lt;/p&gt;
}
else if (State.Value.Error is not null)
{
    &lt;p class="error"&gt;@State.Value.Error&lt;/p&gt;
}
else
{
    &lt;ul&gt;
        @foreach (var p in State.Value.Products)
        {
            &lt;li&gt;@p.Name&lt;/li&gt;
        }
    &lt;/ul&gt;
}

@code {
    protected override void OnInitialized()
        =&gt; Dispatcher.Dispatch(new LoadCatalogAction());
}
</code></pre>
<p>The component no longer contains API calls, error handling or loading logic: it <strong>renders a state</strong> and <strong>emits intents</strong>.</p>
<h2>6. Architectural decisions to make</h2>
<h3>6.1 What goes in the store, and what does not</h3>
<p>The classic trap is putting everything in the store. A pragmatic decision grid:</p>
<ul>
<li><strong>In the store</strong>: data shared across several screens, server data you want to cache, workflow state (cart, multi-step wizard), user preferences.</li>
<li><strong>Outside the store</strong>: purely local, short-lived component state (open menu, value of a field being typed, active tab), values derivable from the URL, data with no value in being traced.</li>
</ul>
<p>If a piece of state is read by a single component and has no reason to outlive it, keep it local.</p>
<h3>6.2 Blazor Server or WebAssembly</h3>
<p>The hosting mode changes the scope of the store:</p>
<ul>
<li><strong>Blazor WebAssembly</strong>: the store lives in the browser, one per tab. This is the closest to Redux on the JavaScript side.</li>
<li><strong>Blazor Server</strong>: the store is registered per circuit (scoped), meaning per connected user session. It must <strong>never</strong> be a singleton, or state will leak between users.</li>
<li><strong>Mixed interactive rendering (.NET 8 and later)</strong>: state must be rebuildable when switching between render modes. Persist what is necessary (for example with <code>PersistentComponentState</code>) and rehydrate the store at initialization.</li>
</ul>
<h3>6.3 Selectors and derived data</h3>
<p>Avoid duplicating computable values in state: a cart total is computed from its lines, not stored. Fluxor offers selectors (<code>FeatureStateSelector</code>) to expose derived views and limit unnecessary renders: a component subscribed to a selector is notified only when the selected value changes.</p>
<h3>6.4 Feature granularity</h3>
<p>Splitting by business domain (Catalog, Cart, Session, Notifications) works better than splitting by screen. Features communicate through actions, not direct references: one feature can react to another’s action through a reducer or an effect without knowing its internal state.</p>
<h3>6.5 Persistence</h3>
<p>To keep state between sessions (preferences, drafts), you can persist selected features to <code>localStorage</code> through an effect that listens to relevant actions, and rehydrate at startup. Never persist sensitive data in the browser, and version the format to handle evolution.</p>
<h2>7. Testing</h2>
<p>This is one of the major gains of the approach. Since reducers are pure functions, they can be tested without any component framework:</p>
<pre><code class="language-csharp">[Fact]
public void OnSuccess_StopsLoading_And_StoresProducts()
{
    var initial = new CatalogState(true, null, []);
    var products = new List&lt;Product&gt; { new(1, "Kettlebell 16 kg") };

    var result = CatalogReducers.OnSuccess(initial, new LoadCatalogSuccessAction(products));

    Assert.False(result.IsLoading);
    Assert.Single(result.Products);
}
</code></pre>
<p>Effects are tested with a mocked dependency (a mock of <code>ICatalogApi</code>) and a spy dispatcher that checks the emitted actions. Components are tested with bUnit by injecting a predefined state: you verify that a given state produces a given render.</p>
<h2>8. Observability and debugging</h2>
<p>Because the action flow is explicit, it becomes observable. With <code>UseReduxDevTools()</code>, the browser’s Redux DevTools extension shows every action, the state before and after, and lets you navigate through history (<em>time travel</em>). For an enterprise project, it is also a good anchor for telemetry: a middleware can log actions to Application Insights or OpenTelemetry, giving a clear correlation between user journeys and application behavior.</p>
<h2>9. Common pitfalls</h2>
<ul>
<li><strong>Impure reducers</strong>: a call to <code>DateTime.Now</code> or <code>Guid.NewGuid()</code> inside a reducer breaks predictability and testing. Generate those values upstream, in the effect or the component, and carry them in the action.</li>
<li><strong>State mutation</strong>: exposing mutable <code>List&lt;T&gt;</code> in a <code>record</code> ruins immutability. Prefer <code>IReadOnlyList&lt;T&gt;</code> or <code>ImmutableList&lt;T&gt;</code>.</li>
<li><strong>Overly technical actions</strong>: <code>SetIsLoadingAction</code> describes a mechanic, not a business event. Prefer actions that say what happened (<code>CatalogLoadRequested</code>, <code>ProductAddedToCart</code>).</li>
<li><strong>A catch-all store</strong>: putting every UI boolean in it multiplies code without benefit.</li>
<li><strong>Excessive renders</strong>: a component subscribed to a large state re-renders on every change. Use selectors or split components.</li>
<li><strong>Underestimated boilerplate</strong>: one action, one reducer and sometimes an effect for a trivial change is the price of traceability. It has to be worth it.</li>
</ul>
<h2>10. When not to use Redux</h2>
<p>Redux is a tool, not a goal. For a simple application with a few screens and little shared state, a scoped service with a change notification event is cheaper and perfectly sufficient. The pattern pays off when:</p>
<ul>
<li>several teams contribute to the same application;</li>
<li>state is shared across many components or screens;</li>
<li>user journeys are asynchronous and complex (workflows, synchronization, real time);</li>
<li>traceability and diagnostics are strong requirements.</li>
</ul>
<p>Other approaches exist in the .NET ecosystem, such as the MVU (Model-View-Update) pattern with Blazor, or lighter state containers. The right selection criterion is long-term maintenance cost, not what is fashionable.</p>
<h2>11. Redux in the age of AI agents</h2>
<p>When an AI agent acts on your application’s state, the Redux store becomes the boundary between what the AI proposes and what the application accepts. This section describes what that changes, and when it is worth it.</p>
<h3>11.1 The problem: a non-deterministic actor</h3>
<p>An LLM-based agent produces variable outputs, sometimes wrong, sometimes out of scope. Giving it direct access to mutable services or component state is like letting a brilliant but unpredictable intern modify production. The Flux pattern offers a structural answer: the agent never writes to state, it <strong>emits actions</strong>. Reducers, pure and deterministic, decide what actually changes.</p>
<h3>11.2 Actions as a tool catalog</h3>
<p>Typed actions form a closed vocabulary of what the application can do. This vocabulary translates almost directly into <strong>tools</strong> exposed to the agent (function calling or MCP): the action’s name and parameters provide the schema, and the compiler guarantees consistency on the C# side. Three useful rules:</p>
<ul>
<li><strong>Allowlist</strong>: expose only a subset of actions to the agent, never the whole catalog.</li>
<li><strong>Intents rather than commands</strong>: the agent emits <em>proposal</em> actions (<code>ChangeProposedAction</code>), not actions that directly modify business data.</li>
<li><strong>Upstream validation</strong>: the effect receiving the agent’s output checks parameters (existence, permissions, bounds) before emitting the action.</li>
</ul>
<h3>11.3 Example: the agent proposes, the user decides</h3>
<p>Consider an assistant that suggests adding a product to the cart. The agent does not modify the cart: it proposes, and the user approves or rejects. Actions describe each step explicitly.</p>
<pre><code class="language-csharp">public record ChangeProposal(Guid Id, string Description, CartItem Item);

public record UserAskedAssistantAction(string Prompt);
public record ChangeProposedAction(ChangeProposal Proposal);   // emitted after the agent call
public record ChangeApprovedAction(ChangeProposal Proposal);   // emitted by the user
public record ChangeRejectedAction(Guid ProposalId);           // emitted by the user
public record ProductAddedToCartAction(CartItem Item);         // business action
</code></pre>
<p>The assistant’s state contains only what the interface needs: the pending proposal and a thinking indicator.</p>
<pre><code class="language-csharp">[FeatureState]
public sealed record AssistantState
{
    public bool IsThinking { get; init; }
    public ChangeProposal? Pending { get; init; }
}

public static class AssistantReducers
{
    [ReducerMethod]
    public static AssistantState OnAsk(AssistantState s, UserAskedAssistantAction _)
        =&gt; s with { IsThinking = true, Pending = null };

    [ReducerMethod]
    public static AssistantState OnProposed(AssistantState s, ChangeProposedAction a)
        =&gt; s with { IsThinking = false, Pending = a.Proposal };

    [ReducerMethod]
    public static AssistantState OnApproved(AssistantState s, ChangeApprovedAction _)
        =&gt; s with { Pending = null };

    [ReducerMethod]
    public static AssistantState OnRejected(AssistantState s, ChangeRejectedAction _)
        =&gt; s with { Pending = null };
}
</code></pre>
<p>The effect bridges to the agent, then, only after approval, to the business action.</p>
<pre><code class="language-csharp">public class AssistantEffects(IAgentClient agent, ICartRules rules)
{
    [EffectMethod]
    public async Task OnAsk(UserAskedAssistantAction action, IDispatcher dispatcher)
    {
        var proposal = await agent.ProposeAsync(action.Prompt);

        // Guardrail: the LLM output is validated before it reaches state
        if (rules.IsAllowed(proposal.Item))
            dispatcher.Dispatch(new ChangeProposedAction(proposal));
    }

    [EffectMethod]
    public Task OnApproved(ChangeApprovedAction action, IDispatcher dispatcher)
    {
        dispatcher.Dispatch(new ProductAddedToCartAction(action.Proposal.Item));
        return Task.CompletedTask;
    }
}
</code></pre>
<p>The component displays the pending proposal with two buttons, <strong>Approve</strong> and <strong>Reject</strong>, each emitting the matching action. No decision logic lives in the UI, and no business change goes through without human validation.</p>
<h3>11.4 Streaming, cancellation and errors</h3>
<p>LLM responses arrive as streams. The pattern fits well: actions such as <code>TokenReceivedAction</code>, <code>ResponseCompletedAction</code>, <code>ResponseFailedAction</code> and <code>GenerationCancelledAction</code> describe the lifecycle, the effect manages the stream and the cancellation token, and components simply render the current state. This avoids scattering <code>try/catch</code> blocks and <code>StateHasChanged()</code> calls across the UI.</p>
<h3>11.5 Traceability and evaluation</h3>
<p>The action log is a natural trace of what the agent proposed, what the user accepted and what actually changed. It serves several purposes:</p>
<ul>
<li><strong>Audit</strong>: who decided what, based on which proposal.</li>
<li><strong>Diagnostics</strong>: replay a session to understand unexpected behavior.</li>
<li><strong>Evaluation</strong>: replay the same inputs with another prompt or model version, and compare the proposals.</li>
</ul>
<h3>11.6 When it is not necessary</h3>
<p>A simple chatbot that displays answers does not need Redux: a scoped service calling the LLM is enough. The pattern becomes relevant when the agent <strong>acts</strong> on the application (data changes, workflow triggers) and you need validation, undo or traceability. It also helps indirectly with AI-assisted development: a very conventional organization (one folder per feature, with state, actions, reducers and effects) gives a coding assistant such as GitHub Copilot a clear framework to follow, especially if these conventions are described in an instructions file.</p>
<h2>Conclusion</h2>
<p>Redux brings to Blazor what growing applications lack most: a clear discipline about how state changes. A single store, explicit actions, pure reducers and isolated effects turn scattered state into a readable, testable and observable flow. With Fluxor, the implementation stays lean and integrates naturally with dependency injection and Blazor’s component system.</p>
<p>In the age of AI agents, this discipline takes on an extra dimension: the store becomes the boundary between what the AI proposes and what the application accepts, with a complete trace of decisions.</p>
<p>The real architectural question is therefore not “Redux or not Redux?” but “what is the right perimeter for my application’s shared state?” Once that boundary is drawn, the Flux approach provides a proven framework to enforce it.</p>
<h2>Further reading</h2>
<ul>
<li>Fluxor repository and documentation: <a href="http://github.com/mrpmorris/Fluxor">github.com/mrpmorris/Fluxor</a></li>
<li>Microsoft documentation on state management in Blazor (<a href="http://ASP.NET">ASP.NET</a> Core Blazor state management)</li>
<li>Redux DevTools, the browser extension to inspect actions and state</li>
</ul>
]]></content:encoded></item></channel></rss>