Persisting your users preferences using Blazor and Local Storage
In the last post we saw how you can use TailwindCSS with Blazor to toggle “dark mode” on and off in your web applications.
But it’s a pain for your users to have to do this every time. It would be much nicer if this setting was persisted in some way. That way, they’d get the same “mode” each and every time they visit your site.
Legacy .NET web apps causing you grief?
Build modern, reliable web applications, faster with .NET and Blazor.
Build better .NET web appsLocal Storage to the rescue#
The simplest way to persist preferences like these is in the browser itself, using local storage.
NOTE
Local Storage is part of the Web Storage API, available in all modern browsers, which enables the storage of key/value pairs.
In javascript you can read data from Local Storage using window.localStorage
.
For example:
And store values using the setItem
method:
You can also store entire objects, you just have to serialise them to a string first…
Then deserialize them from the stored string on the way back out…
Now we could use Javascript Interop from our Blazor app to interact with local storage but it’s much more convenient to use the handy Blazored.LocalStorage NuGet package from Chris Sainty.
Follow the instructions here to add and configure Blazored.LocalStorage for your app.
Before we can think about using Local Storage to persist our preferences, we need to do a little refactoring on the implementation we ended up with last time.
Here’s ThemeToggle.Razor as it currently stands…
Note how we’re storing a value for _darkMode
directly in the component, but we’re also storing it somewhere else too, in MainLayout.razor.
So looking at this again, we’re effectively caching the boolean value for whether DarkMode is in use or not in two places.
If we were to then store this in LocalStorage too we’d be holding that value in three places.
This feels like overkill, and will likely require us to jump through unnecessary hoops to keep them all in sync.
In this case I’d lean towards consolidating that state into one place, so we can implement our business rules and logic (such as defaulting to the OS preference or value stored in Local Storage) without fear of repeating ourselves throughout the application.
Refactor shared state into one place (one source of truth)#
Let’s start by creating a simple ProfileService
and a Preferences
record to store the DarkMode preference itself…
ProfileService.cs
The idea is we’ll call ToggleDarkMode
to turn Dark Mode on or off, and we can retrieve the current preference using GetPreferences
.
Let’s start by implementing ToggleDarkMode
.
I’ll make use of C#‘s shiny new record
feature here (because, well… you know… it’s new, and shiny!) and with
expressions to flip Dark Mode on and off…
ProfileService.cs
This gives us a new instance of the Preferences
record with DarkMode set to the opposite of what it was before.
But what are we to do with this new preferences instance? This is where we can lean on LocalStorage to persist our new preferences.
If we inject ILocalStorageService
(from Blazored.LocalStorage) into our ProfileService
we can use it to persist these preferences to Local Storage.
ProfileService.cs
Now Dark Mode will be toggled on or off, then persisted to Local Storage.
Naturally, we want to be able to retrieve these preferences too so I’ve also implemented GetPreferences
to look the preferences up from Local Storage, defaulting to a new instance of Preferences
if nothing has yet been saved.
Register ProfileService
for use via dependency injection#
Now is also a good time to make sure that ProfileService
is registered as a service so we can use it wherever we see fit. How you do this depends on whether you’re using Blazor WASM or Blazor Server.
In this case I’m using Blazor WASM so I need to head over to Program.cs and register the service there:
Access shared state directly from Blazor components#
So far so good, but we’re not actually using this ProfileService
anywhere. To do that we’ll need to make a small tweak to…
ThemeToggle.Razor
We’ve removed the local boolean for _darkMode
and replaced it with a direct call to Toggle Dark Mode by invoking ProfileService.ToggleDarkMode()
.
Note we’ve also removed the OnDarkModeToggled
EventCallback parameter, because it’s now redundant.
We don’t want to wait until the user has tried to toggle Dark mode to read these preferences. They may have already switched to dark mode during a previous visit, so we want to be able to read their preferences and apply them whether or not they click the button to toggle dark mode on/off.
We need to fetch this state when the app is rendered for the first time, not just when the user clicks that button. For this reason we don’t need that event callback anymore and will make sure to retrieve the relevant state elsewhere.
NOTE
Inject services into your Blazor components
You can inject a service directly into a Blazor components using this syntax:
This leans on .NET’s dependency injection system. So long as the service is registered, you can inject it into your components, then use it as you see fit.
Which brings us to the next challenge; how do we retrieve the preferences from ProfileService
in order to actually switch themes?
For that we need to head over to MainLayout.razor.
As a reminder, here’s how it currently looks:
The task here is to bring in our ProfileService
and use that to retrieve preferences, then use those preferences to drive whether dark mode is enabled or not.
We’ve made a couple of changes here:
<ThemeToggle />
no longer takes an OnDarkModeToggled
EventCallback Parameter so we’ve removed the code which assigned that.
We retrieve the user’s preferences from ProfileService
during OnInitializedAsync
and store the preferences in a field which our markup then reads to determine whether to switch to Dark Mode or not.
NOTE
Avoid trying to access LocalStorage from OnInitializedAsync
if using Blazor Server
We need to be mindful of one limitation with using Local Storage this way from a Blazor app.
If you’re using Blazor WASM this code will work just fine, but if you’re using Blazor Server you can’t access LocalStorage via javascript during OnInitialized
.
This is because that code runs on the server as part of a prerendering phase, and the server doesn’t have access to your browser’s local storage via javascript (kinda makes sense when you think about it!)
In this case you can achieve the same result, but you’ll need to move the code to OnAfterRenderAsync
.
OnAfterRenderAsync
does not run as part of the prerendering phase on the server, only running once the app is loaded and rendered in the browser.
At this point we can check if this is the firstRender
, and if so retrieve the preferences from local storage, assign them to the _preferences
field then explicitly instruct Blazor that StateHasChanged
so it knows it needs to render again (typically it wouldn’t need to render again after rendering has just finished, hence the need to be explicit).
It’s important to include that firstRender
test because without that we’d get into an infinite loop and all sorts of shenanigans would occur.
Tell Blazor to re-render#
We’re almost there, but run this now, click the button and…
… nothing happens!
Well actually, something does happen, but the theme doesn’t change.
Crack open the developer tools, hit the Application tab (in Chrome/Edge) and you’ll see the local storage being set as you click the button…
But Blazor isn’t re-rendering, and for good reason… we haven’t told it to.
When we click to the toggle button it calls ProfileService
which in turn updates Local Storage, but we never tell the UI to re-render and there’s nothing in place to make this happen automatically.
This is where we can use good old C# events to make sure we can react and trigger a render when preferences have changed.
Here’s an updated version of ProfileService which invokes an event every time preferences are changed.
With this we get a handy event which will be raised each and every time Dark mode is toggled (on or off).
Now we just need to hook into this event in MainLayout.razor to trigger a re-render of our component.
We register ProfileServiceOnChange
as a handler for ProfileService.OnChange
. The result is this method will be invoked every time OnChange
is invoked by code in ProfileService
.
Because the event includes the new preferences as an argument it’s easy to grab those and update the preferences in MainLayout accordingly.
We have to call StateHasChanged
so Blazor knows to re-render at this point.
Note we’ve also implemented IDisposable
and made to sure to unregister the event handler as well. This is good practice to make sure we’re cleaning up after ourselves!
With that we can click the button to toggle dark mode, knowing that we’ll get the new theme we want and the preferences will be stored in local storage for next time.
Respect their OS preferences#
One last thing to consider, your visitors may have set dark mode as their preference for apps at OS level.
Here’s the relevant Windows setting.
It’s pretty straightforward to read this setting using javascript…
If this returns true then they’ve selected dark mode as their preference.
We can access this from Blazor via JS interop…
First we need to write the javascript, in this case we can just add it to index.html (for Blazor WASM) or _host.cshtml (for Blazor Server).
index.html
Now ProfileService can read this when deciding which theme to initially prefer…
In summary#
When you need to store a user’s preferences consider using Local Storage.
To avoid keeping state in multiple places you can use a service to handle getting and setting state, this is also a good place to put any business logic you need to be consistent across multiple use cases. There are other solutions to this challenge of storing state in Blazor which might also be worth a look…
If you need to re-render based on state changes, consider using an event and event callback to react to state changes and re-render your Blazor components accordingly.