DEV Community

loading...

Advanced Blazor State Management Using Fluxor, part 6 - Persisting State

mr_eking profile image Eric King ・5 min read

This is the sixth in a short series of blog posts where I will go beyond the introductory level and dig a bit deeper into using the Fluxor library in a Blazor Wasm project.

Persisting State

In a comment on the previous post in this series, a reader observes that when the application is reloaded, the application state is lost. They then ask how to go about saving the application state so that it remains even after the page is reloaded.

There are undoubtedly many ways to accomplish this; In this post I'll demonstrate one technique.

I'll be using the browser's localStorage API as the mechanism for storing the state of the Counter and the Weather features, but this could work just as well using the IndexedDB API or even calling off to some web API endpoint to save and retrieve state in a database.

To begin, let's note that the localStorage API is a JavaScript API, and isn't directly available to be called by our Blazor code. We need to either write our own JSInterop code to interact with localStorage, or we can take advantage of a library somebody else has already written. I'm going to use the excellent Blazored.LocalStorage.

To install it into our Client project, we start with the NuGet package:

Install-Package Blazored.LocalStorage
Enter fullscreen mode Exit fullscreen mode

And configure it in the Program.cs file:

using Blazored.LocalStorage;

...

builder.Services.AddBlazoredLocalStorage(config => 
{
    config.JsonSerializerOptions.WriteIndented = true;
});
Enter fullscreen mode Exit fullscreen mode

Let's start by saving the CounterState to localStorage.

To do so, I want to have an EffectMethod that subscribes to an Action instructing it to store the State, and which then Dispatches a Success or Failure Action that my application can then respond to.

For the EffectMethod to have access to the Blazored.LocalStorage service, we need to inject it into the CounterEffects class. We should also define a unique string to act as the localStorage key for the CounterState.

private readonly ILocalStorageService _localStorageService;
private const string CounterStatePersistenceName = "BlazorWithFluxor_CounterState";

public CounterEffects(ILocalStorageService localStorageService)
{
    _localStorageService = localStorageService;
}
Enter fullscreen mode Exit fullscreen mode

Now I can add an EffectMethod that can be handed a CounterState for the localStorageService to persist. The service will handle serializing the State for us.

[EffectMethod]
public async Task PersistState(CounterPersistStateAction action, IDispatcher dispatcher) 
{
    try
    {
        await _localStorageService.SetItemAsync(CounterStatePersistenceName, action.CounterState);
        dispatcher.Dispatch(new CounterPersistStateSuccessAction());
    }
    catch (Exception ex) 
    {
        dispatcher.Dispatch(new CounterPersistStateFailureAction(ex.Message));
    }
}

Enter fullscreen mode Exit fullscreen mode

This means we need 3 new actions:

public class CounterPersistStateAction
{
    public CounterState CounterState { get; }
    public CounterPersistStateAction(CounterState counterState)
    {
        CounterState = counterState;
    }
}
public class CounterPersistStateSuccessAction { }
public class CounterPersistStateFailureAction
{
    public string ErrorMessage { get; }
    public CounterPersistStateFailureAction(string errorMessage)
    {
        ErrorMessage = errorMessage;
    }
}

Enter fullscreen mode Exit fullscreen mode

We can then repeat the process for Loading the state, and for completeness we may also want to Clear the State.

[EffectMethod(typeof(CounterLoadStateAction))]
public async Task LoadState(IDispatcher dispatcher)
{
    try
    {
        var counterState = await _localStorageService.GetItemAsync<CounterState>(CounterStatePersistenceName);
        if (counterState is not null) 
        {
            dispatcher.Dispatch(new CounterSetStateAction(counterState));
            dispatcher.Dispatch(new CounterLoadStateSuccessAction());
        }
    }
    catch (Exception ex)
    {
        dispatcher.Dispatch(new CounterLoadStateFailureAction(ex.Message));
    }
}

[EffectMethod(typeof(CounterClearStateAction))]
public async Task ClearState(IDispatcher dispatcher)
{
    try
    {
        await _localStorageService.RemoveItemAsync(CounterStatePersistenceName);
        dispatcher.Dispatch(new CounterSetStateAction(new CounterState { CurrentCount = 0 }));
        dispatcher.Dispatch(new CounterClearStateSuccessAction());
    }
    catch (Exception ex)
    {
        dispatcher.Dispatch(new CounterClearStateFailureAction(ex.Message));
    }
}
Enter fullscreen mode Exit fullscreen mode

These EffectMethods need to affect the value of the CounterState, so we need a ReducerMethod to handle the CounterSetStateAction.

[ReducerMethod]
public static CounterState OnCounterSetState(CounterState state, CounterSetStateAction action) 
{
    return action.CounterState;
}
Enter fullscreen mode Exit fullscreen mode

With these in place, we are able to persist and retrieve the CounterState whenever is appropriate for our application by Dispatching the corresponding Actions.

For this scenario, let's assume we want to read and apply the CounterState when the application loads. Let's accomplish this by creating a Razor Component that has the responsibility of retrieving persisted state. we create a component named StateLoader.razor and place in it the following code:

@inherits FluxorComponent

@using BlazorWithFluxor.Client.Features.Counter.Store

@inject IDispatcher Dispatcher

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        if (firstRender)
        {
            Dispatcher.Dispatch(new CounterLoadStateAction());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And we can now reference this component in the MainLayout.razor file so that it's loaded at application start.

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <div class="main">
        <div class="top-row px-4">
            <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
        </div>

        <div class="content px-4">
            @Body
        </div>
    </div>
</div>

<BlazoredToasts Position="ToastPosition.TopRight" Timeout="15" />

<StateLoader />

Enter fullscreen mode Exit fullscreen mode

This will load the CounterState from localStorage if it exists. Let's now give the Counter.razor component a way to Save, Load, and Clear the State by adding a few buttons beside the existing Increment button:

<button class="btn btn-primary" @onclick="IncrementCount">Increment</button>
<button class="btn btn-outline-primary" @onclick="SaveCount">Save</button>
<button class="btn btn-outline-info" @onclick="LoadCount">Load</button>
<button class="btn btn-outline-danger" @onclick="ClearCount">Clear</button>
Enter fullscreen mode Exit fullscreen mode

and their click handlers:

private void SaveCount()
{
    Dispatcher.Dispatch(new CounterPersistStateAction(CounterState.Value));
}

private void LoadCount()
{
    Dispatcher.Dispatch(new CounterLoadStateAction());
}

private void ClearCount()
{
    Dispatcher.Dispatch(new CounterClearStateAction());
}
Enter fullscreen mode Exit fullscreen mode

And with that, we're set.

Note: For demonstration purposes I have hooked in the Blazored.Toast component to pop up a Toast message whenever the CounterState is saved or retrieved, as demonstrated in the previous post in this series. For brevity's sake I'm not including that code in this post, but you can find it all in the project's repository on github.

When the Save button is clicked, we can see the CounterState is saved to the browser's Local Storage:

Demonstration of state Save

And if the page is reloaded after the state has been saved, the application will load the saved state:

Demonstration of state Load on app Load

When the Load button is clicked, state is retrieved from storage and the Store's state is replaced with it:

Demonstration of state Load on button click

And the Clear button will remove the state from storage and reset the current count:

Demonstration of state Clear

One thing to note about Local Storage is that it's not secure, and it can be easily and directly manipulated right in the browser's developer tools:

Demonstration of state manually altering state data in Local Storage

Which means it can be manipulated in a way that results in badly formatted data:

Demonstration of state manually altering state data in Local Storage with bad data

So if you need to persist the state in a secure manner, you'll need to use something other than Local Storage.

If we repeat the same set of actions for the WeatherStore, we can see that Blazored.LocalStorage can handle more complicated objects too, including collections of objects like the Forecasts array.

Alt Text

Please leave a comment if you're enjoying this series or if you have suggestions for more posts.

As always, happy coding!

Discussion (10)

pic
Editor guide
Collapse
padnom profile image
padnom • Edited

Great job and thx for that's!
I performed a similar approach but instead of have a save and load action.
I persist state, each time state changed and I load form local storage only when counter.state.value is null.

Collapse
mr_eking profile image
Eric King Author

I'm glad you got it working. There's probably a million ways to go about it.

But I do have a question... I'm wondering when Counter.State.Value is null? If Fluxor is initialized correctly, no State should ever be null. You provide a non-null initial State in the Feature.

Collapse
padnom profile image
padnom

You right. It was a shorthand to explain what I do
In my code I use it for display username (string)
var login = loginSate.Value.LoginBackOffice;
if (login.Status == EnumStateLogin.NotInstantiate)
{
return await loginSessionStorage.GetLoginAsync(); ;
}
return login;

Collapse
thomeijer profile image
Thomas Meijer

Thanks for the great series, I really enjoyed all six posts.

I was wondering how you would go about 'cross user state'. For example, if there are two users active at the same time, and a weather forecast was added by user 1, how would you notify user 2 that a weather forecast was added.

Would it be possible to use a Singleton store where all users can dispatch actions to and subscribe to application wide actions?

Collapse
mr_eking profile image
Eric King Author

FYI I added a simple example using SignalR to the repo for this series. It doesn't persist anything to a database (I'm trying to keep the project as simple as possible) but it does demonstrate how two clients can communicate via Actions. I'll add another blog post about it soon.

github.com/eric-king/BlazorWithFluxor

Collapse
thomeijer profile image
Thomas Meijer

That looks exactly like something I was looking for, thanks a bunch!

Collapse
mr_eking profile image
Eric King Author • Edited

Thanks for the kind words.

In this scenario, the application state is all located in the client, as it's a Blazor WebAssembly application. There's no shared Fluxor state at all, as each client has its own store.

If I were to add cross-client communication so that each client's state could be updated based on another client's actions, I don't think I would do it via Fluxor on the server. I would do it via SignalR on the server, and my "singleton store" would be a database.

User1 submits some change to the server (which gets stored in the database), and that process notifies the server's SignalR hub of the change. The SignalR hub broadcasts the change to all of the listening clients, which would include User2. And User2's listener then Dispatches the appropriate Action to update their Store and whatever UI changes are needed.

Collapse
tailslide profile image
Tailslide

I made a little library to persist fluxor states.. you can find it on nuget under Fluxor.Persist or here:

github.com/Tailslide/fluxor-persist

Collapse
christianweyer profile image
Christian Weyer

Great article series, kudos!

I realize you are using a lot of XYZSuccessAction and XYZFailureAction types. And it seems like a common pattern. Wouldn't it be a good idea to have those in Fluxor as SuccessAction and FailureAction?

Thanks again.

Collapse
mr_eking profile image
Eric King Author

Thanks Christian.

It seems to me that there would be very limited value in a generic SuccessAction or FailureAction. Wouldn't you want to know what failed or succeeded, so you could react appropriately? If you only had a generic SuccessAction, then to know what succeeded you would need a payload of some sort identifying the Action that succeeded. To react to a particular success, you would have to handle the generic SuccessAction, which means processing every success and deciding whether to act on it by inspecting the payload.

I would much rather dispatch a specific XYZSuccessAction and be able to react to that specific action if necessary.

There's no reason you can't also dispatch a generic SuccessAction if you wish to do something generic upon every success, but I can't think of a reason I'd want to do that at the moment.