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
And configure it in the Program.cs
file:
using Blazored.LocalStorage;
...
builder.Services.AddBlazoredLocalStorage(config =>
{
config.JsonSerializerOptions.WriteIndented = true;
});
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;
}
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));
}
}
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;
}
}
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));
}
}
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;
}
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());
}
}
}
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 />
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>
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());
}
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:
And if the page is reloaded after the state has been saved, the application will load the saved state:
When the Load
button is clicked, state is retrieved from storage and the Store's state is replaced with it:
And the Clear
button will remove the state from storage and reset the current count:
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:
Which means it can be manipulated in a way that results in badly formatted 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.
Please leave a comment if you're enjoying this series or if you have suggestions for more posts.
As always, happy coding!
Top comments (11)
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?
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
That looks exactly like something I was looking for, thanks a bunch!
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.
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.
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.
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;
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
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.
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.
Thanks for a great series! It definitely helps my understanding when you have added so many simple examples that haven't been included in the other well-known tutorials. I would be interested in seeing if you could make an extra part to the series involving nested objects and avoiding any potential pitfalls
Some comments may only be visible to logged-in visitors. Sign in to view all comments.