State Management with Blazor using Fluxor (Part 2)

blazor

673 views

State Management with Blazor using Fluxor (Part 2) blog meme

Memes aside, if you've been following along with our flux-ified Blazor project, you may have noticed the plethora of initial, success, and failure action types. While this may seem like overkill to some, by creating this separation of concern between action types, we ensure each action has a single responsibility in the flux workflow. If (read: when) our business requirements change that cause us to modify existing actions, we can do so without fear of unintentionally breaking another workflow that might reuse the same action. It's like I always say, "when in doubt, create a new action."

Continuing where we left off

In our last post, we discussed what the flux design pattern is, what problems it seeks to solve, and how to integrate it with Blazor using the Fluxor library. So far, we've created two action workflows to interact with the JSON Placeholder API: a todo list retrieve workflow and a todo retrieval by ID workflow. With the help of a few services and effects, we successfully implemented the R of our CRUD operations using actions, reducers, and our central application store to allow our components to simply get only the state features they care about and display them on the page. In this (hopefully shorter) post, we'll do a bit of service refactoring and add a few more CRUD operations to give us a functioning application that'll allow us to interface with the todos from the JSON Placeholder API.

Refactoring to typed HTTP services

To kick things off, let's start by refactoring how we've been calling the API. So far, we've been injecting an HttpClient instance directly into our effects with the help of the DI container from the Microsoft.Extensions.DependencyInjection library. A more robust, unit test-able approach would be to utilize a centralized service that contains the HttpClient instance injected into it so that we can configure a single client instance to our liking (attach headers, set base paths, etc.) and funnel all external network calls into a single place, rather than have HttpClients spread throughout all of our flux effects. In our Services folder at the root of our project, let's add a file named JsonPlaceholderApiService.cs with the following code:

JsonPlaceholderApiService.cs

using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;

namespace StateManagementWithFluxor.Services
{
    public class JsonPlaceholderApiService
    {
        private readonly ILogger<JsonPlaceholderApiService> _logger;
        private readonly HttpClient _httpClient;

        public JsonPlaceholderApiService(ILogger<JsonPlaceholderApiService> logger, HttpClient httpClient) =>
            (_logger, _httpClient) = (logger, httpClient);

        public Task<TResponse> GetAsync<TResponse>(string path)
        {
            _logger.LogInformation($"GET: Retrieving resource of type {typeof(TResponse).Name}");
            return _httpClient.GetFromJsonAsync<TResponse>(path);
        }

        public Task<HttpResponseMessage> PostAsync<TBody>(string path, TBody body)
        {
            _logger.LogInformation($"POST: Creating resource of type {typeof(TBody).Name}");
            return _httpClient.PostAsJsonAsync(path, body);
        }

        public Task<HttpResponseMessage> PutAsync<TBody>(string path, TBody body)
        {
            _logger.LogInformation($"PUT: Updating resource of type {typeof(TBody).Name}");
            return _httpClient.PutAsJsonAsync(path, body);
        }

        public Task<HttpResponseMessage> DeleteAsync(string path)
        {
            _logger.LogInformation("DELETE: Removing resource");
            return _httpClient.DeleteAsync(path);
        }
    }
}

With our centralized HTTP service in place, we've effectively created a typed client that we can configure within the DI container to inject into our effects, with a configured HTTP client service wired up at runtime. Back in our Program.cs, we'll need to add a service reference to our JsonPlaceholderApiService, but before doing so, let's add a package reference to the Microsoft.Extensions.Http NuGet package so that we can properly configure the HTTP client that will be injected into this service. I'm using Visual Studio's package manager to add the reference to our project, but feel free to use the command line (dotnet add package Microsoft.Extensions.Http), or the Package Manager console (Install-Package Microsoft.Extensions.Http). Once we've successfully added the package, let's add the following code to Program.cs:

Program.cs

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Fluxor;
using System.Reflection;
using StateManagementWithFluxor.Services;
using System.Net.Mime;

namespace StateManagementWithFluxor
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            // Add Fluxor
            builder.Services.AddFluxor(options =>
            {
                options.ScanAssemblies(Assembly.GetExecutingAssembly());
                options.UseReduxDevTools();
            });

            // Add custom application services
            builder.Services.AddScoped<StateFacade>();
            builder.Services.AddHttpClient<JsonPlaceholderApiService>(client =>
            {
                client.DefaultRequestHeaders.Add("Content-Control", $"{MediaTypeNames.Application.Json}; charset=utf-8");
                client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com");
            });

            await builder.Build().RunAsync();
        }
    }
}

From the above, we can see that we've added the line builder.Services.AddHttpClient<JsonPlaceholderApiService> configuring the HttpClient instance that will be injected into the API service for us at runtime. Every time a JsonPlaceholderApiService instance is requested at runtime within our effects, the DI container will inject a configured HttpClient instance with the base address we've pointed to here and the default headers on each request. With our API service wired up, let's go ahead and add some markup to allow users to create todos. When creating a todo and sending the corresponding POST request to JSON Placeholder API, we won't actually be creating a resource on the server (we'll see the consequences of this later).

The Placeholder API is simply just that - a dummy API that acts as a placeholder for realtime requests and responses. Our create request will send back a 201 with the created todo resource. We'll take that todo and simply append it to our current list of todos and re-render the list within our Todos.razor component to reflect the addition of the newly created todo. In a real world scenario, this isn't really something you'd want to be doing on the frontend. A better solution, and more common in practice, would be to send a create request to a downstream server from the client, and upon a successful status returned from the server, we'd send another request to the server to retrieve the list again that should contain our newly created resource. There's a few reason as to why we would implement the design in this way, but a bit irrelevant and out of scope for this post.

Let's go ahead create a Components folder at our solution root and within that directory, let's add a razor file called CreateTodoForm.razor with the following markup:

CreateTodoForm.razor

@inherits Fluxor.Blazor.Web.Components.FluxorComponent

@using StateManagementWithFluxor.Models.Todos.Validation
@using StateManagementWithFluxor.Services

@inject StateFacade Facade

<EditForm Model="validationModel" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    <div class="d-flex flex-column align-items-center">
        <ValidationSummary />
    </div>

    <div class="d-flex flex-row justify-content-center">
        <div class="form-group px-2">
            <label for="create-todo-title">Title</label>
            <InputText  id="create-todo-title"  @bind-Value="validationModel.Title" class="form-control"/>
        </div>
        <div class="form-group px-2">
            <label for="create-todo-completed">User ID</label>
            <InputNumber ParsingErrorMessage="That is not a valid user ID" id="create-todo-user-id" @bind-Value="validationModel.UserId" class="form-control"/>
        </div>
        <div class="form-group px-2 mt-3 d-flex flex-column align-items-center">
            <label for="create-todo-completed">Completed?</label>
            <InputCheckbox id="create-todo-completed" @bind-Value="validationModel.Completed" class="form-check"/>
        </div>
    </div>

    <div class="pt-2 d-flex flex-row justify-content-center">
        <button class="btn btn-primary" type="submit">Create</button>
    </div>
</EditForm>


@code {

    private CreateOrUpdateTodoValidationModel validationModel = new CreateOrUpdateTodoValidationModel();

    protected override void OnInitialized()
    {
        validationModel.UserId = 1;

        base.OnInitialized();
    }

    private void HandleValidSubmit()
    {
        // We use the bang operator (!) to tell the compiler we'll know this string field will not be null
        Facade.CreateTodo(validationModel.Title!, validationModel.Completed, validationModel.UserId);
    }

}

In our CreateTodoForm.razor component, we're simply just adding a bit of form markup to allow users to input a todo title, user ID, and the current status of the todo (done/not done). We're using Blazor's built-in EditForm to perform field validations on our model that we'll ultimately send to the server containing the todo data, again letting these built-in Blazor tools handle the heavy input validation lifting for us. Using the DataAnnotationsValidator and ValidationSummary components, we're able to dynamically render validation messages when the user input does not meet our criteria for handling form submission. Using a little Bootstrap, we'll be able to display our error messages in a somewhat pleasant manner as well.

Notice that we've added a new model type with the CreateOrUpdateTodoValidationModel reference that will have it's properties set as we've bound it to our EditForm using the Model attribute. Let's go ahead and add a Validation folder underneath the Todos feature folder within our Models directory. Within the Validation folder, let's add the CreateOrUpdateTodoValidationModel.cs class file:

CreateOrUpdateTodoValidationModel.cs

using System.ComponentModel.DataAnnotations;

namespace StateManagementWithFluxor.Models.Todos.Validation
{
    public class CreateOrUpdateTodoValidationModel
    {
        [Required(AllowEmptyStrings = false, ErrorMessage = "Your todo must have a title")]
        public string? Title { get; set; }

        [Required(ErrorMessage = "Status of this todo is required")]
        public bool Completed { get; set; }

        [Required(ErrorMessage = "User ID associated with this todo is required")]
        [Range(1, 100)]
        public int UserId { get; set; }
    }
}

Using .NET's System.ComponentModel.DataAnnotations library, we add a few Required attributes to each of the properties, with an additional Range attribute on the UserId to enforce users stay within the 1 to 100 user ID values. While it might seem a bit overkill to add this validation model when we could just simply bind each of the properties to a model to be sent off to a server, we have to keep in mind that we don't want our models to become god objects. If you've ever worked on a large enterprise codebase, you've more than likely seen this at some point or another - API models reused across multiple endpoint request payloads trying to fit the mold everywhere, "helper" classes that only "help" you magically break this unintentionally when making slight changes, and the list goes on. Our request models and validation models should be responsible for one thing, and one thing only - the CreateOrUpdateTodoValidationModel is only concerned about validating input.

Back in our CreateTodoForm.razor with the EditForm referencing our validationModel, we bind each of the properties within the validationModel to an input tag. Luckily for us, the .NET team has provider a few nice custom input components when dealing with EditForms within our application. We bind each of the Title, Completed, and UserId properties to a wrapping input component in InputText, InputCheckbox, and InputNumber respectively. Adding a few form-control Bootstrap classes and we've got a semi-decent looking form!

In our OnInitialized component lifecycle method, we initialize the user ID to 1 since the rendered markup with be a type="number" input. We've also added another method to our StateFacade service with CreateTodo - let's go ahead and add that to the facade:

StateFacade.cs

using Fluxor;
using Microsoft.Extensions.Logging;
using StateManagementWithFluxor.Models.Todos.Dtos;
using StateManagementWithFluxor.Store.Features.Todos.Actions.CreateTodo;


namespace StateManagementWithFluxor.Services
{
    public class StateFacade
    {
        // Previous constructor and methods...

        public void CreateTodo(string title, bool completed, int userId)
        {
            // Construct our validated todo
            var todoDto = new CreateOrUpdateTodoDto(title, completed, userId);

            _logger.LogInformation($"Issuing action to create todo [{title}] for user [{userId}]");
            _dispatcher.Dispatch(new CreateTodoAction(todoDto));
        }
    }
}

If you're curious about why we've added this "facade" service, I mention the reasons for doing so in the first post. Our CreateTodo method simply constructs a CreateOrUpdateTodoDto immutable request model and issues a CreateTodoAction with it as the payload. Let's add each of those classes to their respective folders:

Models/Todos/Dtos/CreateOrUpdateTodoDto.cs

namespace StateManagementWithFluxor.Models.Todos.Dtos
{
    public class CreateOrUpdateTodoDto
    {
        public CreateOrUpdateTodoDto(string title, bool completed, int userId) =>
            (Title, Completed, UserId) = (title, completed, userId);

        public string Title { get; }

        public bool Completed { get; }

        public int UserId { get; }
    }
}

Store/Features/Todos/Actions/CreateTodo/CreateTodoAction.cs

using StateManagementWithFluxor.Models.Todos.Dtos;

namespace StateManagementWithFluxor.Store.Features.Todos.Actions.CreateTodo
{
    public class CreateTodoAction
    {
        public CreateTodoAction(CreateOrUpdateTodoDto todo) =>
            Todo = todo;

        public CreateOrUpdateTodoDto Todo { get; }
    }
}

With our newly created action CreateTodoAction, let's add our success and failure actions while we're here before creating the reducer methods that will subscribe to these actions:

CreateTodoSuccessAction.cs

using StateManagementWithFluxor.Models.Todos.Dtos;

namespace StateManagementWithFluxor.Store.Features.Todos.Actions.CreateTodo
{
    public class CreateTodoSuccessAction
    {
        public CreateTodoSuccessAction(TodoDto todo) =>
            Todo = todo;

        public TodoDto Todo { get; }
    }
}

CreateTodoFailureAction.cs

using StateManagementWithFluxor.Store.Features.Shared.Actions;

namespace StateManagementWithFluxor.Store.Features.Todos.Actions.CreateTodo
{
    public class CreateTodoFailureAction : FailureAction
    {
        public CreateTodoFailureAction(string errorMessage)
            : base(errorMessage)
        {
        }
    }
}

Recall from our previous post that we created a common FailureAction for all of our failure type actions to descend from, allowing us to store common error properties within this shared action for all failure actions to tap into. With our create action in place, let's add a reducer to our our Reducers directory within our todos feature folder to handle the create actions:

Store/Features/Reducers/CreateTodoActionsReducer.cs

using Fluxor;
using StateManagementWithFluxor.Models.Todos.Dtos;
using StateManagementWithFluxor.Store.Features.Todos.Actions.CreateTodo;
using StateManagementWithFluxor.Store.State;
using System.Collections.Generic;
using System.Linq;

namespace StateManagementWithFluxor.Store.Features.Todos.Reducers
{
    public static class CreateTodoActionsReducer
    {
        [ReducerMethod]
        public static TodosState ReduceCreateTodoAction(TodosState state, CreateTodoAction _) =>
            new TodosState(true, null, state.CurrentTodos, state.CurrentTodo);

        [ReducerMethod]
        public static TodosState ReduceCreateTodoSuccessAction(TodosState state, CreateTodoSuccessAction action)
        {
            // Grab a reference to the current todo list, or initialize one if we do not currently have any loaded
            var currentTodos = state.CurrentTodos is null ?
                new List<TodoDto>() :
                state.CurrentTodos.ToList();

            // Add the newly created todo to our list and sort by ID
            currentTodos.Add(action.Todo);
            currentTodos = currentTodos
                .OrderBy(t => t.Id)
                .ToList();

            return new TodosState(false, null, currentTodos, state.CurrentTodo);
        }

        [ReducerMethod]
        public static TodosState ReduceCreateTodoFailureAction(TodosState state, CreateTodoFailureAction action) =>
            new TodosState(false, action.ErrorMessage, state.CurrentTodos, state.CurrentTodo);
    }
}

When an initial CreateTodoAction is dispatched, we set the TodoState with the loading property set to true and the existing todos and selected todo, as we don't want to alter the current state those actions ultimately produce as our create workflow has no concern with either workflow. Upon a successful creation, i.e. the API returning a successful 201 status code back to us, we'll produce a new TodosState by appending the newly created todo item to a copy of the current list of todos, and set the sorted list within the constructor of the TodosState object. Finally, upon a CreateTodoFailureAction being dispatched, we keep the currently loaded todos piece of state intact, making sure to produce a new TodosState containing an error that we issued the failure action with.

With our reducer in place, we'll need to add a side effect (if you're a little fuzzy on the flux workflow, checkout the first post) that will do the actual calling of the API using our JsonPlaceholderApiService and issue either a success, or failure, action based on the response. In our Effects directory underneath our todos feature folder, let's add a CreateTodoEffect.cs file:

CreateTodoEffect.cs

using Fluxor;
using Microsoft.Extensions.Logging;
using StateManagementWithFluxor.Models.Todos.Dtos;
using StateManagementWithFluxor.Services;
using StateManagementWithFluxor.Store.Features.Todos.Actions.CreateTodo;
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;

namespace StateManagementWithFluxor.Store.Features.Todos.Effects.CreateTodo
{
    public class CreateTodoEffect : Effect<CreateTodoAction>
    {
        private readonly ILogger<CreateTodoEffect> _logger;
        private readonly JsonPlaceholderApiService _apiService;

        public CreateTodoEffect(ILogger<CreateTodoEffect> logger, JsonPlaceholderApiService httpClient) =>
            (_logger, _apiService) = (logger, httpClient);

        protected override async Task HandleAsync(CreateTodoAction action, IDispatcher dispatcher)
        {
            try
            {
                _logger.LogInformation($"Creating todo {action.Todo}...");
                var createResponse = await _apiService.PostAsync("todos", action.Todo);

                if (!createResponse.IsSuccessStatusCode)
                {
                    throw new HttpRequestException($"Error creating todo: {createResponse.ReasonPhrase}");
                }

                _logger.LogInformation("Todo created successfully!");
                var createdTodo = await createResponse.Content.ReadFromJsonAsync<TodoDto>();
                dispatcher.Dispatch(new CreateTodoSuccessAction(createdTodo));
            }
            catch (Exception e)
            {
                _logger.LogError($"Could not create todo, reason: {e.Message}");
                dispatcher.Dispatch(new CreateTodoFailureAction(e.Message));
            }
        }
    }
}

Nothing too out of the ordinary here, but let's do a quick code breakdown:

  • We inject a logger and our API service from the DI container, using an expression-bodied tuple constructor (C# syntactic sugar)
  • Using our API service, we call the PostAsync method passing in the todo from the CreateTodoAction payload and await for the response
  • If we get back a good looking 201, we'll go ahead and dispatch a CreateTodoSuccessAction with the todo response returned from the API as the payload
  • Upon any unchecked exceptions, or any non-successful status code returned from the server, we'll dispatch an error action with the corresponding error message from the exception

With our create workflow in place, let's go ahead and add our CreateTodoForm.razor component to our Todos.razor page. In the markup section of Todos.razor, we'll replace the current markup with the following:

Todos.razor

<h3 class="text-center mb-3">Todos</h3>

@if (TodosState.Value.IsLoading)
{
    <div class="d-flex flex-row justify-content-center">
        <div class="spinner-border" role="status">
            <span class="sr-only">Loading...</span>
        </div>
    </div>
}
else if (TodosState.Value.HasCurrentErrors || TodosState.Value.CurrentTodos is null)
{
    <div class="d-flex flex-column align-items-center">
        <span class="py-2">Whoops! Looks like an issue occurred loading todos :(</span>
        <button class="btn btn-info py-2" @onclick="() => Facade.LoadTodos()">Reload Todos</button>
    </div>
}
else if (!TodosState.Value.CurrentTodos.Any())
{
    <CreateTodoForm />

    <div class="d-flex flex-column align-items-center mt-2">
        <span class="py-2">Looks like there's no todos...</span>
        <button class="btn btn-info py-2" @onclick="() => Facade.LoadTodos()">Reload Todos</button>
    </div>
}
else
{
    <CreateTodoForm />

    <table class="table table-hover mt-2">
        <thead>
            <tr class="text-center">
                <th scope="col">ID</th>
                <th scope="col">Title</th>
                <th scope="col">Completed</th>
                <th scope="col">User ID</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var todo in TodosState.Value.CurrentTodos)
            {
                <tr style="cursor: pointer;" class="text-center" @onclick="() => NavigateToTodoDetail(todo.Id)">
                    <td>@todo.Id</td>
                    <td>@todo.Title</td>
                    <td>@todo.Completed</td>
                    <td>@todo.UserId</td>
                </tr>
            }
        </tbody>
    </table>
}

We've added a reference to our create todo form with the <CreateTodoForm /> markup component in this razor page, so when we navigate to the page, we should see a simple form after the todos have been loaded from the API:

Create_todo_Form

Notice we've added the <CreateTodoForm /> component in two sections of our markup, once when a rendered list is pulled from the store, and again when there are no todos loaded available in state. With the help of Fluxor, we can easily render these conditional pieces of markup based on our current state value, tapping into TodosState.Value to access our properties we've added to that particular piece of state. Internally, Fluxor will call StateHasChanged for us when we have components descending from a FluxorComponent anytime the our store published a new TodosState (triggered by the store), signaling Blazor to re-render the current markup based on our new TodosState value. If you're familiar with Angular, StateHasChanged is the analog to the ChangeDetectorRef service commonly utilized by Angular components that turn off automatic change detection when ChangeDetectionStrategy.OnPush is used for that component's change detection strategy.

With our form in place, let's test our create workflow to make sure everything works as expected. With Redux Dev Tools open in the dev console, let's go ahead and fire up our application. When it's up and running, navigate to the Todos nav menu item, and we should a few actions fire off, namely the LoadTodosAction and the LoadTodosSuccessAction ( hopefully). Inspecting these actions, we can look at the action payloads, the current Todos state value, and the computed diff between states once the action was dispatched. If we fill in some values for the create todo form on the top of the page and hit the submit button, we should again see two more actions dispatched in the form of CreateTodoAction carrying the form data we entered in the action payload, and a corresponding CreateTodoSuccessAction computed state diff with an extra todo item added to the CurrentTodos property state. The change should then be reflected in our table, since the store pushed out two corresponding TodosState changes, causing Blazor to re-render its current markup twice and rendering the corresponding HTML based on the values within our TodosState.

Updating todos

With our create workflow in place, let's iron out our update workflow. Since we've already put in place our retrieve workflows, all we really need to do is add a bit code code to the TodoDetail.razor page to allow users to modify the todo values when they've been selected from the todos table. To kick things off, let's add some actions for updating a todo. In our Actions folder within our Todos feature folder, let's add an UpdateTodo folder. Inside that directory, we'll add three actions corresponding to the kickoff, success, and failure actions:

UpdateTodoAction.cs

using StateManagementWithFluxor.Models.Todos.Dtos;

namespace StateManagementWithFluxor.Store.Features.Todos.Actions.UpdateTodo
{
    public class UpdateTodoAction
    {
        public UpdateTodoAction(int id, CreateOrUpdateTodoDto todo) =>
            (Id, Todo) = (id, todo);

        public int Id { get; }

        public CreateOrUpdateTodoDto Todo { get; }
    }
}

UpdateTodoSuccessAction.cs

using StateManagementWithFluxor.Models.Todos.Dtos;

namespace StateManagementWithFluxor.Store.Features.Todos.Actions.UpdateTodo
{
    public class UpdateTodoSuccessAction
    {
        public UpdateTodoSuccessAction(TodoDto todo) =>
            Todo = todo;

        public TodoDto Todo { get; }
    }
}

UpdateTodoFailureAction.cs

using StateManagementWithFluxor.Store.Features.Shared.Actions;

namespace StateManagementWithFluxor.Store.Features.Todos.Actions.UpdateTodo
{
    public class UpdateTodoFailureAction : FailureAction
    {
        public UpdateTodoFailureAction(string errorMessage)
            : base(errorMessage)
        {
        }
    }
}

Similar to our create workflow, we've allowed users to update todos using our CreateOrUpdateTodoDto class we created to keep consistent with the properties they can use to create and update todos. With our actions in place, let's add the corresponding reducer methods that will compute the state upon dispatch of these actions. In our Reducers folder, let's add an UpdateTodoActionsReducer.cs class file:

UpdateTodoActionsReducer.cs

using Fluxor;
using StateManagementWithFluxor.Models.Todos.Dtos;
using StateManagementWithFluxor.Store.Features.Todos.Actions.UpdateTodo;
using StateManagementWithFluxor.Store.State;
using System.Collections.Generic;
using System.Linq;

namespace StateManagementWithFluxor.Store.Features.Todos.Reducers
{
    public static class UpdateTodoActionsReducer
    {
        [ReducerMethod]
        public static TodosState ReduceUpdateTodoAction(TodosState state, UpdateTodoAction _) =>
            new TodosState(true, null, state.CurrentTodos, state.CurrentTodo);

        [ReducerMethod]
        public static TodosState ReduceUpdateTodoSuccessAction(TodosState state, UpdateTodoSuccessAction action)
        {
            // If the current todos list is null, set the state with a new list containing the updated todo
            if (state.CurrentTodos is null)
            {
                return new TodosState(false, null, new List<TodoDto> { action.Todo }, state.CurrentTodo);
            }

            // Rather than mutating in place, let's construct a new list and add our updated item
            var updatedList = state.CurrentTodos
                .Where(t => t.Id != action.Todo.Id)
                .ToList();

            // Add the todo and sort the list
            updatedList.Add(action.Todo);
            updatedList = updatedList
                .OrderBy(t => t.Id)
                .ToList();

            return new TodosState(false, null, updatedList, null);
        }

        [ReducerMethod]
        public static TodosState ReduceUpdateTodoFailureAction(TodosState state, UpdateTodoFailureAction action) =>
            new TodosState(false, action.ErrorMessage, state.CurrentTodos, state.CurrentTodo);
    }
}

Nothing too exciting with our ReduceUpdateTodoAction and ReduceUpdateTodoFailureAction state reducers, as they simply compute a new TodosState based on the action payload. Upon a successful update action being dispatched, our ReduceUpdateTodoSuccessAction will create a new list based on the CurrentTodos values, excluding the value we've updated, add the updated todo from the UpdateTodoSuccessAction payload, and create a new TodosState with this new list. Again, we probably wouldn't do this in a real world production application, where we might opt instead to recall the API to rehydrate the current list view.

With our reducer in place, let's add a couple side effects for the UpdateTodoAction and UpdateTodoSuccessAction being dispatched. In our Effects folder, let's add an UpdateTodo subfolder with two effects, UpdateTodoEffect.cs and UpdateTodoSuccessEffect.cs:

UpdateTodoEffect.cs

using Fluxor;
using Microsoft.Extensions.Logging;
using StateManagementWithFluxor.Models.Todos.Dtos;
using StateManagementWithFluxor.Services;
using StateManagementWithFluxor.Store.Features.Todos.Actions.UpdateTodo;
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;

namespace StateManagementWithFluxor.Store.Features.Todos.Effects.UpdateTodoEffect
{
    public class UpdateTodoEffect : Effect<UpdateTodoAction>
    {
        private readonly ILogger<UpdateTodoEffect> _logger;
        private readonly JsonPlaceholderApiService _apiService;

        public UpdateTodoEffect(ILogger<UpdateTodoEffect> logger, JsonPlaceholderApiService httpClient) =>
            (_logger, _apiService) = (logger, httpClient);

        protected override async Task HandleAsync(UpdateTodoAction action, IDispatcher dispatcher)
        {
            try
            {
                _logger.LogInformation($"Updating todo {action.Id}...");
                var updateResponse = await _apiService.PutAsync($"todos/{action.Id}", action.Todo);

                if (!updateResponse.IsSuccessStatusCode)
                {
                    throw new HttpRequestException($"Error updating todo: {updateResponse.ReasonPhrase}");
                }

                _logger.LogInformation("Todo updated successfully!");
                var updatedTodo = await updateResponse.Content.ReadFromJsonAsync<TodoDto>();
                dispatcher.Dispatch(new UpdateTodoSuccessAction(updatedTodo));
            }
            catch (Exception e)
            {
                _logger.LogError($"Could not update todo, reason: {e.Message}");
                dispatcher.Dispatch(new UpdateTodoFailureAction(e.Message));
            }
        }
    }
}

UpdateTodoSuccessEffect.cs

using Fluxor;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using StateManagementWithFluxor.Store.Features.Todos.Actions.UpdateTodo;
using System.Threading.Tasks;

namespace StateManagementWithFluxor.Store.Features.Todos.Effects.UpdateTodoEffect
{
    public class UpdateTodoSuccessEffect : Effect<UpdateTodoSuccessAction>
    {
        private readonly ILogger<UpdateTodoSuccessEffect> _logger;
        private readonly NavigationManager _navigation;

        public UpdateTodoSuccessEffect(ILogger<UpdateTodoSuccessEffect> logger, NavigationManager navigation) =>
            (_logger, _navigation) = (logger, navigation);

        protected override Task HandleAsync(UpdateTodoSuccessAction action, IDispatcher dispatcher)
        {
            _logger.LogInformation("Updated todo successfully, navigating back to todo listing...");
            _navigation.NavigateTo("todos");

            return Task.CompletedTask;
        }
    }
}

Our UpdateTodoEffect.cs utilizes our JsonPlaceholderApiService to make a PUT call to the API using the action's payload (our CreateOrUpdateTodoDto object), and issues an update success action upon a successful response from the API. In our UpdateTodoSuccessEffect, we navigate the user back to the todos page, where the list should now display the updated values we've just entered and submitted on the todo details form. We choose to navigate users back to the todos page only on a successful action dispatched, rather than navigating users back regardless, as we want to inform users of unexpected errors while still on the details page before navigating back to the page. We're also controlling the user's navigation, building in predictable UX for the user rather than having them navigate back manually, or navigating else where in our application that we might not expect during this workflow.

With our core flux workflow in place for the update actions, let's add a method to our StateFacade so that we can issue these actions from our TodosDetail.razor page. In our StateFacade service:

StateFacade.cs

// Previously existing imported namespaces...
using StateManagementWithFluxor.Store.Features.Todos.Actions.UpdateTodo;

namespace StateManagementWithFluxor.Services
{
    public class StateFacade
    {
        // Previous facade methods...

        public void UpdateTodo(int id, string title, bool completed, int userId)
        {
            // Construct our validated todo
            var todoDto = new CreateOrUpdateTodoDto(title, completed, userId);

            _logger.LogInformation($"Issuing action to update todo {id}");
            _dispatcher.Dispatch(new UpdateTodoAction(id, todoDto));
        }
    }
}

With our UpdateTodo facade method in place, let's finally add to the markup in TodoDetail.razor to dispatch the update action and allow users to modify the currently selected todo values:

TodoDetail.razor

@inherits Fluxor.Blazor.Web.Components.FluxorComponent

@page "/todos/{todoId}"

@using StateManagementWithFluxor.Models.Todos.Validation
@using StateManagementWithFluxor.Store.State
@using StateManagementWithFluxor.Services

@inject IState<TodosState> TodosState
@inject StateFacade Facade

<h3 class="text-center mb-3">Detail for todo item @TodoId</h3>

@if (TodosState.Value.IsLoading)
{
    <div class="d-flex flex-row justify-content-center">
        <div class="spinner-border" role="status">
            <span class="sr-only">Loading...</span>
        </div>
    </div>
}
else if (TodosState.Value.HasCurrentErrors || TodosState.Value.CurrentTodo is null)
{
    <div class="d-flex flex-column align-items-center">
        <span>Whoops! Looks like an issue occurred loading todos :(</span>
        <div class="py-3">
            <a class="btn btn-primary" href="/todos">Back to Todos</a>
        </div>
    </div>
}
else
{
    <EditForm Model="validationModel" OnValidSubmit="HandleValidSubmit">
        <DataAnnotationsValidator />
        <div class="d-flex flex-column align-items-center">
            <ValidationSummary />
        </div>

        <div class="d-flex flex-column align-items-center py-1">
            <div class="d-flex flex-row py-2">
                <strong>ID</strong>: @TodosState.Value.CurrentTodo.Id
            </div>
            <div class="py-2 d-flex flex-row align-items-center">
                <strong class="px-2">Completed:</strong>
                <InputCheckbox class="form-check" @bind-Value="validationModel.Completed" />
            </div>
            <div class="py-2 d-flex flex-row align-items-center">
                <strong class="px-2">User:</strong>
                <InputNumber class="form-control" @bind-Value="validationModel.UserId"/>
            </div>
            <div class="py-2 d-flex flex-row align-items-center">
                <strong class="px-2">Title:</strong>
                <InputText class="form-control" @bind-Value="validationModel.Title"/>
            </div>

            <div class="d-flex flex-row justify-content-center pt-2">
                <button type="submit" class="btn btn-secondary">Update</button>
                <span class="px-2"></span>
                <button class="btn btn-danger">Delete</button>
            </div>

            <div class="py-3">
                <a class="btn btn-primary" href="/todos">Back to Todos</a>
            </div>
        </div>
    </EditForm>

}

@code {

    private CreateOrUpdateTodoValidationModel validationModel = new CreateOrUpdateTodoValidationModel();

    [Parameter]
    public string? TodoId { get; set; }

    protected override void OnInitialized()
    {
        // Load the todo detail on initial page navigation
        if (int.TryParse(TodoId, out var parsedId))
        {
            Facade.LoadTodoById(parsedId);
        }

        // Register a state change to assign the validation fields
        TodosState.StateChanged += (sender, state) =>
        {
            if (state.CurrentTodo is null)
            {
                return;
            }

            validationModel.Title = state.CurrentTodo.Title;
            validationModel.Completed = state.CurrentTodo.Completed;
            validationModel.UserId = state.CurrentTodo.UserId;

            StateHasChanged();
        };

        base.OnInitialized();
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
    }

    private void HandleValidSubmit()
    {
        // We use the bang operator (!) to tell the compiler we'll know this string field will not be null
        Facade.UpdateTodo(TodosState.Value.CurrentTodo!.Id, validationModel.Title!, validationModel.Completed, validationModel.UserId);
    }

}

Since we've changed quite a few things in this detail page, let's do a quick code breakdown:

  • First, we've added an EditForm to handle validating the user's input when updating values similar to how we've setup our CreateTodoForm.razor component
  • We've added a CreateOrUpdateTodoValidationModel validation model that will react to user input and display any validation errors with the help of the <DataAnnotationsValidator /> and <ValidationSummary /> built-in Blazor components
  • We've modified our OnInitialized lifecycle method to react to state changes so we can reassign the validation model fields from the current todo fields and trigger an additional StateHasChanged() event to tell Blazor to re-render the markup to reflect these changes within the inputs of the form
  • Since we're subscribing to additional state changes outside of the scope of Fluxor, we need make sure to manually dispose of our component to avoid memory leaks; again, if you're familiar with Angular, this is similar to destroying your observable subscriptions when a component is disposed
  • Upon a valid submit, we'll issue the UpdateTodo facade method that will dispatch the UpdateTodoAction and kickoff the update workflow

Awesome, now that our todo detail page has been updated, if we select a todo from the todos table, we should now be able to update that particular todo's values and see those changes reflected back in the table once we hit submit (and the API responds successfully). I'll leave it as an exercise for the reader to see what actions are dispatched in the Redux Dev Tools console panel and inspect the actions, payloads, and diffs that are computed.

Deleting todos

With the light at the end of the tunnel now within sight, we just need to add one more workflow to delete todos. Thankfully, most of the internal plumbing we've already done, so we'll only have to hookup the delete actions to the delete button we've added to our detail page. Let's start by adding a DeleteTodos folder within our Actions directory, with three actions in DeleteTodoAction.cs, DeleteTodoSuccessAction.cs, DeleteTodoFailureAction.cs:

DeleteTodoAction.cs

namespace StateManagementWithFluxor.Store.Features.Todos.Actions.DeleteTodo
{
    public class DeleteTodoAction
    {
        public DeleteTodoAction(int id) =>
            Id = id;

        public int Id { get; }
    }
}

DeleteTodoSuccessAction.cs

namespace StateManagementWithFluxor.Store.Features.Todos.Actions.DeleteTodo
{
    public class DeleteTodoSuccessAction
    {
        public DeleteTodoSuccessAction(int id) =>
            Id = id;

        public int Id { get; }
    }
}

DeleteTodoFailureAction.cs

using StateManagementWithFluxor.Store.Features.Shared.Actions;

namespace StateManagementWithFluxor.Store.Features.Todos.Actions.DeleteTodo
{
    public class DeleteTodoFailureAction : FailureAction
    {
        public DeleteTodoFailureAction(string errorMessage)
            : base(errorMessage)
        {
        }
    }
}

From the above actions, we see that we're simply only transporting the todo ID in the kickoff and success action payloads, as we only need a reference to the ID to remove the todo from our list. While it might be tempting to reuse the kickoff and success actions as they are effectively the same class, the overhead of adding actions is rather small and doesn't add much complexity. If our requirements change, maybe the business analysts want a message transmitted upon a successful delete, we'll only be refactoring a single action sub-workflow (the success flow for the delete todo workflow) with zero fear of unintentionally breaking the kickoff sub-workflow. With our actions in place, let's add our last reducer in our Reducers folder with DeleteTodoActionsReducer.cs:

DeleteTodoActionsReducer.cs

using Fluxor;
using StateManagementWithFluxor.Store.Features.Todos.Actions.DeleteTodo;
using StateManagementWithFluxor.Store.State;
using System.Linq;

namespace StateManagementWithFluxor.Store.Features.Todos.Reducers
{
    public static class DeleteTodoActionsReducer
    {
        [ReducerMethod]
        public static TodosState ReduceDeleteTodoAction(TodosState state, DeleteTodoAction _) =>
            new TodosState(true, null, state.CurrentTodos, state.CurrentTodo);

        [ReducerMethod]
        public static TodosState ReduceDeleteTodoSuccessAction(TodosState state, DeleteTodoSuccessAction action)
        {
            // Return the default state if no list of todos is found
            if (state.CurrentTodos is null)
            {
                return new TodosState(false, null, null, state.CurrentTodo);
            }

            // Create a new list with all todo items excluding the todo with the deleted ID
            var updatedTodos = state.CurrentTodos.Where(t => t.Id != action.Id);

            return new TodosState(false, null, updatedTodos, state.CurrentTodo);
        }

        [ReducerMethod]
        public static TodosState ReduceDeleteTodoFailureAction(TodosState state, DeleteTodoFailureAction action) =>
            new TodosState(false, action.ErrorMessage, state.CurrentTodos, state.CurrentTodo);
    }
}

Again, nothing special with our ReduceDeleteTodoAction and ReduceDeleteTodoFailureAction, as they're simply just computing action-based stated. With our ReduceDeleteTodoSuccessAction, we grab a reference to a list with the help of LINQ containing all todos excluding the todo who's ID matches the ID from the issued action. Using this newly created list, we'll create a new TodosState using this list and notify our components. With our reducer in place, let's add a few side effects to help with calling the API with DeleteTodoEffect.cs and DeleteTodoSuccessEffect.cs. In our Effects folder, let's add a DeleteTodos subfolder with the following classes:

DeleteTodoEffect.cs

using Fluxor;
using Microsoft.Extensions.Logging;
using StateManagementWithFluxor.Services;
using StateManagementWithFluxor.Store.Features.Todos.Actions.DeleteTodo;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace StateManagementWithFluxor.Store.Features.Todos.Effects.DeleteTodo
{
    public class DeleteTodoEffect : Effect<DeleteTodoAction>
    {
        private readonly ILogger<DeleteTodoEffect> _logger;
        private readonly JsonPlaceholderApiService _apiService;

        public DeleteTodoEffect(ILogger<DeleteTodoEffect> logger, JsonPlaceholderApiService apiService) =>
            (_logger, _apiService) = (logger, apiService);

        protected override async Task HandleAsync(DeleteTodoAction action, IDispatcher dispatcher)
        {
            try
            {
                _logger.LogInformation($"Deleting todo {action.Id}...");
                var deleteResponse = await _apiService.DeleteAsync($"todos/{action.Id}");

                if (!deleteResponse.IsSuccessStatusCode)
                {
                    throw new HttpRequestException($"Error deleting todo: {deleteResponse.ReasonPhrase}");
                }

                _logger.LogInformation($"Todo deleted successfully!");
                dispatcher.Dispatch(new DeleteTodoSuccessAction(action.Id));
            }
            catch (Exception e)
            {
                _logger.LogError($"Could not create todo, reason: {e.Message}");
                dispatcher.Dispatch(new DeleteTodoFailureAction(e.Message));
            }
        }
    }
}

DeleteTodoSuccessEffect.cs

using Fluxor;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using StateManagementWithFluxor.Store.Features.Todos.Actions.DeleteTodo;
using System.Threading.Tasks;

namespace StateManagementWithFluxor.Store.Features.Todos.Effects
{
    public class DeleteTodoSuccessEffect : Effect<DeleteTodoSuccessAction>
    {
        private readonly ILogger<DeleteTodoSuccessEffect> _logger;
        private readonly NavigationManager _navigation;

        public DeleteTodoSuccessEffect(ILogger<DeleteTodoSuccessEffect> logger, NavigationManager navigation) =>
            (_logger, _navigation) = (logger, navigation);

        protected override Task HandleAsync(DeleteTodoSuccessAction action, IDispatcher dispatcher)
        {
            _logger.LogInformation("Deleted todo successfully, navigating back to todo listing...");
            _navigation.NavigateTo("todos");

            return Task.CompletedTask;
        }
    }
}

With our DeleteTodoEffect, we setup an eerily similar workflow to our other issued actions, simply using our API service to call the DELETE endpoint with the ID from the action's payload. On a successful response we'll issue the success action that will trigger the DeleteTodoSuccessEffect to fire off, navigating the user back to the todos page with a new version of the table without the todo we just deleted. Again, we use effects as a form of navigation control to selectively guide users around our application in a predicable manner. With our effects in place, let's add a facade method to help facilitate the dispatching of the delete actions from our components. Back in our StateFacade service:

StateFacade.cs

// Previously existing imported namespaces...
using StateManagementWithFluxor.Store.Features.Todos.Actions.DeleteTodo;

namespace StateManagementWithFluxor.Services
{
    public class StateFacade
    {
        // Previous facade methods...

        public void DeleteTodo(int id)
        {
            _logger.LogInformation($"Issuing action to delete todo {id}");
            _dispatcher.Dispatch(new DeleteTodoAction(id));
        }
    }
}

With our facade method in place, let's finally add the interaction to the delete button on our todo detail page with just a simple @onclick event handler within our EditForm markup block:

TodoDetail.razor

<EditForm Model="validationModel" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    <div class="d-flex flex-column align-items-center">
        <ValidationSummary />
    </div>

    <div class="d-flex flex-column align-items-center py-1">
        <div class="d-flex flex-row py-2">
            <strong>ID</strong>: @TodosState.Value.CurrentTodo.Id
        </div>
        <div class="py-2 d-flex flex-row align-items-center">
            <strong class="px-2">Completed:</strong>
            <InputCheckbox class="form-check" @bind-Value="validationModel.Completed" />
        </div>
        <div class="py-2 d-flex flex-row align-items-center">
            <strong class="px-2">User:</strong>
            <InputNumber class="form-control" @bind-Value="validationModel.UserId"/>
        </div>
        <div class="py-2 d-flex flex-row align-items-center">
            <strong class="px-2">Title:</strong>
            <InputText class="form-control" @bind-Value="validationModel.Title"/>
        </div>

        <div class="d-flex flex-row justify-content-center pt-2">
            <button type="submit" class="btn btn-secondary">Update</button>
            <span class="px-2"></span>
            <button class="btn btn-danger" @onclick="() => Facade.DeleteTodo(TodosState.Value.CurrentTodo.Id)">Delete</button>
        </div>

        <div class="py-3">
            <a class="btn btn-primary" href="/todos">Back to Todos</a>
        </div>
    </div>
</EditForm>

Notice we've only just added the @onclick="() => Facade.DeleteTodo(TodosState.Value.CurrentTodo.Id)" event handler to the delete button to fire off the initial delete action based on the ID of the currently displayed todo.

Whew... that was quite a bit of code. Let's sit back and enjoy the fruits of our labor and test out the delete workflow. If we navigate to the todos page to see the list of displayed todos, go ahead and select a todo. Once we're navigated to the detail page, with the Redux Dev tools console open, go ahead and hit the delete button. What actions do we see issued in the console? Checkout the diffs and the action payloads for a little extra credit. Once we see the DeleteTodoSuccessAction issued, we're navigated back to our todos page and see the table has been updated to reflect the removal of our todo! Pretty cool, huh?

Wrapping up

In the final stretch, we've seen one strategy for implementing CRUD workflows with the help of Fluxor in our Blazor applications. Bringing the redux experience to Blazor empowers us .NET developers to build bulletproof and predictable Blazor applications simplifying the complex problem of state management on the frontend, bringing a tried-and-true design patter in flux along with us. Feel free to play around with the code we've written so far, and maybe even improve the workflows we already have set in place (see what happens when you reload the todo detail with a selected todo and try to delete it). All the code we've written so far is available on GitHub for those following along. Next up, we'll explore automated integration testing on our todos application using Cypress to help further strengthen our codebase and provide yet another tool we can bring into the development process to assist with bulletproofing our workflows, along with a few considerations and potential refactors we could make with our current application architecture to make it a bit more robust.

Until next time, friends!

Now listening

Spotify listening to

Big And Chunky

will.i.am