loading

Write and read values

SetItem / GetItem / RemoveItem

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.

C#
@inject Bit.Butil.LocalStorage localStorage

await localStorage.SetItem("theme", "dark");

var theme = await localStorage.GetItem("theme"); // "dark"

await localStorage.RemoveItem("theme");
Live sample
Key
Value
localStorage output
Results will appear here when you interact with the samples.

Session storage

SessionStorage

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.

C#
@inject Bit.Butil.SessionStorage sessionStorage

await sessionStorage.SetItem("wizard-step", "2");

var step = await sessionStorage.GetItem("wizard-step");

await sessionStorage.RemoveItem("wizard-step");
Live sample
Key
Value
sessionStorage output
Results will appear here when you interact with the samples.

Typed values

SetItem<T> / GetItem<T>

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.

C#
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 == 14
Live sample
Theme
Font size
typed values output
Results will appear here when you interact with the samples.

Inspect the store

GetLength / GetKey / ContainsKey

Enumerate 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.

C#
var count = await localStorage.GetLength();

for (var i = 0; i < count; i++)
{
    var key = await localStorage.GetKey(i);
}

var hasTheme = await localStorage.ContainsKey("theme");
Live sample
Key index
Key to check
inspect output
Results will appear here when you interact with the samples.

Clear everything

Clear

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.

C#
await localStorage.Clear();

await sessionStorage.Clear();
Live sample
clear output
Results will appear here when you interact with the samples.

Cross-tab change events

SubscribeChanges

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.

C#
private ButilSubscription? subscription;

subscription = await localStorage.SubscribeChanges(e =>
{
    Console.WriteLine($"{e.Key}: {e.OldValue} -> {e.NewValue}");
});

// later, e.g. in DisposeAsync:
await subscription.DisposeAsync();
Live sample
change events output
Results will appear here when you interact with the samples.
Note:
localStorage vs sessionStorage Same API, different lifetime. 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.
Warning:
Storage events are cross-tab only The 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)

Member
Signature
Description
GetLength
Task<int> GetLength()
Returns the number of data items stored in the storage area.
GetKey
Task<string?> GetKey(int index)
Returns the name of the nth key in the storage, or null when the index is out of range.
ContainsKey
Task<bool> ContainsKey(string key)
True when the storage contains an item with the given key.
GetItem
Task<string?> GetItem(string? key)
Returns the raw string value stored under the key, or null when absent.
GetItem<T>
Task<T?> GetItem<T>(string key, JsonSerializerOptions? options = null)
Returns a JSON-deserialized value, or default(T) when the key is missing. Throws JsonException when the stored value is not valid JSON for T.
SetItem
Task SetItem(string? key, string? value)
Adds the key to the storage, or updates its value if it already exists.
SetItem<T>
Task SetItem<T>(string key, T? value, JsonSerializerOptions? options = null)
JSON-serializes the value and stores it under the key. Strings are stored as-is.
RemoveItem
Task RemoveItem(string? key)
Removes the key from the storage. Removing a missing key is a no-op.
Clear
Task Clear()
Empties all keys out of the storage area.
SubscribeChanges
Task<ButilSubscription> SubscribeChanges(Action<StorageEvent> handler)
Subscribes to cross-tab storage events for this storage area. Dispose the returned subscription to detach the handler.
StorageEvent
class StorageEvent { string? Key; string? OldValue; string? NewValue; string? Url; string StorageArea; }
Event payload: the changed key (null on clear), previous and new values, the URL of the document that triggered the change, and the storage area name.
InvokeStorageEvent
void InvokeStorageEvent(Guid id, StorageEvent evt)
Infrastructure - JSInvokable callback dispatched from JavaScript when a storage event fires. Not intended to be called from application code.
DisposeAsync
ValueTask DisposeAsync()
Detaches all active change subscriptions and releases the JS interop reference.
An unhandled error has occurred. Reload 🗙