Local & Session Storage
Synchronous key/value storage scoped to the origin. LocalStorage survives browser restarts; SessionStorage lives and dies with the tab. Both are exposed through the same ButilStorage API, so everything on this page applies to either service.
@inject Bit.Butil.LocalStorage localStorageMDN reference
The core of the Storage API: store a string under a key, read it back, or remove it. Values written to localStorage persist until explicitly removed - close the browser, come back tomorrow, and they are still there.
@inject Bit.Butil.LocalStorage localStorage
await localStorage.SetItem("theme", "dark");
var theme = await localStorage.GetItem("theme"); // "dark"
await localStorage.RemoveItem("theme");SessionStorage shares the exact same API but is scoped to the current tab: a page reload keeps the data, closing the tab discards it, and two tabs of the same site each get their own copy. It is ideal for wizard state, draft forms and other per-visit data.
@inject Bit.Butil.SessionStorage sessionStorage
await sessionStorage.SetItem("wizard-step", "2");
var step = await sessionStorage.GetItem("wizard-step");
await sessionStorage.RemoveItem("wizard-step");Browsers only store strings, so Butil offers generic overloads that JSON-serialize on write and deserialize on read. Strings round-trip untouched; any other type goes through System.Text.Json. Only read typed values you wrote with the typed overload - raw strings written via SetItem may not be valid JSON.
public record UserPrefs(string Theme, int FontSize);
await localStorage.SetItem("prefs", new UserPrefs("dark", 14));
var prefs = await localStorage.GetItem<UserPrefs>("prefs");
// prefs.Theme == "dark", prefs.FontSize == 14Enumerate a storage area without knowing its keys up front: GetLength reports how many items are stored, GetKey returns the name of the nth key, and ContainsKey checks for a key's existence without materializing its value.
var count = await localStorage.GetLength();
for (var i = 0; i < count; i++)
{
var key = await localStorage.GetKey(i);
}
var hasTheme = await localStorage.ContainsKey("theme");Clear empties the entire storage area for this origin in one call. It only affects the area you call it on - clearing localStorage leaves sessionStorage untouched, and vice versa.
await localStorage.Clear();
await sessionStorage.Clear();The DOM storage event fires when another tab or window of the same origin modifies localStorage. SubscribeChanges wires a C# handler to it and returns a ButilSubscription you dispose to detach. To see it in action, subscribe below, open this site in a second tab, and write a value there.
private ButilSubscription? subscription;
subscription = await localStorage.SubscribeChanges(e =>
{
Console.WriteLine($"{e.Key}: {e.OldValue} -> {e.NewValue}");
});
// later, e.g. in DisposeAsync:
await subscription.DisposeAsync();localStorage persists across restarts and is shared by
every tab of the origin; sessionStorage is per-tab and cleared when the tab closes.
Both are limited to roughly 5 MB per origin and store strings only.
storage event never fires in the tab that made the change, and it only propagates
across tabs for localStorage - sessionStorage is scoped to a single tab,
so its subscription will not receive cross-tab notifications.
API reference (ButilStorage - shared by LocalStorage and SessionStorage)
Task<int> GetLength()Task<string?> GetKey(int index)Task<bool> ContainsKey(string key)Task<string?> GetItem(string? key)Task<T?> GetItem<T>(string key, JsonSerializerOptions? options = null)Task SetItem(string? key, string? value)Task SetItem<T>(string key, T? value, JsonSerializerOptions? options = null)Task RemoveItem(string? key)Task Clear()Task<ButilSubscription> SubscribeChanges(Action<StorageEvent> handler)class StorageEvent { string? Key; string? OldValue; string? NewValue; string? Url; string StorageArea; }void InvokeStorageEvent(Guid id, StorageEvent evt)ValueTask DisposeAsync()