Keeping Multiple Blazor Tabs in Sync with the Browser Broadcast Channel API

 


A user opens your Blazor application in one browser tab. Then they open a second tab, perhaps to compare information or keep two parts of the application visible at the same time.

They change something in the first tab.

Nothing happens in the second tab.

This is expected. Each browser tab has its own page, JavaScript context, and Blazor Server circuit. Even though both tabs are displaying the same application for the same user, they don’t automatically share live state.

You could solve this with SignalR, a server-side notification service, or a distributed messaging system. For many applications, however, that would be far more infrastructure than the problem requires.

When the communication only needs to happen between tabs from the same application in the same browser, the browser already provides a lightweight solution: the Broadcast Channel API.

In this article, we will examine:

  • What developers sometimes mean by the “Broadcast API”

  • What the Broadcast Channel API actually is

  • When it is a good fit

  • Why it is such an interesting browser capability

  • How to implement it in a Blazor Server application

  • How to combine it with localStorage and JavaScript interoperability

  • Where its limitations begin and when to use SignalR instead


What Is the “Broadcast API”?

Developers sometimes informally call this feature the Broadcast API, but the official browser feature is named the Broadcast Channel API.

The API exposes a JavaScript interface named:

BroadcastChannel

A BroadcastChannel allows different browser contexts from the same origin to exchange messages.

Those browser contexts can include:

  • Browser tabs

  • Separate browser windows

  • Same-origin iframes

  • Web workers

  • Service workers

You can think of it as a very small, browser-managed publish-and-subscribe system.

A page joins a named channel:

const channel = new BroadcastChannel("application-events");

It can then publish a message:

channel.postMessage({
    type: "counter-changed",
    value: 5
});

Other browser contexts that have joined the same named channel can receive that message:

channel.onmessage = event => {
    console.log(event.data);
};

The browser handles delivering the message to the other listeners.

There is no Web API endpoint to call, no database to poll, and no server-side message broker to configure.


What Is the Broadcast Channel API?

The Broadcast Channel API is a browser API for sending messages between same-origin browsing contexts.

A channel is identified by a string name:

const channel = new BroadcastChannel("counter-demo");

Any other compatible browser context can join that channel by using the same name:

const channel = new BroadcastChannel("counter-demo");

When one context publishes a message, the browser raises a message event for the other channel instances.

Messages are sent with postMessage():

channel.postMessage({
    eventType: "counter-updated",
    counterValue: 10
});

Messages can contain more than strings. The Broadcast Channel API uses the browser’s structured clone algorithm, which supports many JavaScript data types without requiring the developer to manually convert every message to a JSON string.

When a channel is no longer needed, it should be closed:

channel.close();

Closing the channel disconnects that channel object and allows its resources to be reclaimed.


Same-Origin Communication

The most important Broadcast Channel rule is that participating pages must have the same origin.

An origin consists of the page’s:

  • Scheme

  • Host

  • Port

For example, these two pages have the same origin:

https://example.com/counter
https://example.com/weather

These do not:

https://example.com
https://admin.example.com

Even though both domains belong to the same organization, their hosts are different.

These also have different origins:

http://example.com
https://example.com

The schemes are different.

Different ports also create different origins:

https://localhost:5001
https://localhost:7001

The API intentionally limits communication to same-origin contexts. A tab from an unrelated website cannot simply join your application’s channel and listen to its messages.


A Simple Broadcast Channel Example

Before adding Blazor, consider the browser-only implementation.

Sending a message

const channel = new BroadcastChannel("demo-channel");

channel.postMessage({
    type: "status-changed",
    status: "Complete",
    changedAt: new Date().toISOString()
});

Receiving a message

const channel = new BroadcastChannel("demo-channel");

channel.addEventListener("message", event => {
    console.log("Message received:", event.data);
});

Open the application in two tabs. When the first tab sends a message, the second tab receives it.

The API feels almost too simple, which is part of what makes it useful.


Why Is It Pretty Cool?

The Broadcast Channel API solves a real application problem with very little code.

It does not require a server round trip

The message stays within the user’s browser. The first tab does not need to send an event to your application server so that the server can send it back to the second tab.

That makes the communication fast and removes unnecessary server traffic.

It works between otherwise isolated tabs

Browser tabs normally operate independently. The API creates a controlled communication mechanism between them without requiring one tab to have a direct reference to the other.

With window.postMessage(), developers often need a reference to another window, such as a popup or parent frame. A broadcast channel is based on a shared name instead.

It supports structured messages

You are not limited to broadcasting a simple string.

You can send an object that describes the event:

{
    type: "preferences-updated",
    source: "settings-page",
    payload: {
        theme: "dark",
        pageSize: 50
    }
}

This makes it possible to create a small event contract for your application.

It separates the sender from the receiver

The sending page does not need to know which tabs are open.

It publishes an event to a named channel. Interested tabs listen to that channel and decide what to do with the message.

That is a simple form of decoupling.

It is a browser capability, not a framework feature

The Broadcast Channel API is not specific to Blazor, React, Angular, or Vue.

The same technique can be used across different front-end technologies, as long as the pages run under the same origin.


The Blazor Server Challenge

In Blazor Server, application logic executes on the server. Browser events and browser APIs, however, execute in the browser.

That creates a boundary:

Blazor component
      |
      | JavaScript interoperability
      v
Browser JavaScript
      |
      v
BroadcastChannel API

Blazor cannot directly instantiate a browser BroadcastChannel from C#.

Instead:

  1. A Blazor component calls JavaScript.

  2. JavaScript creates or accesses the channel.

  3. JavaScript listens for browser messages.

  4. When a message arrives, JavaScript calls a .NET method.

  5. The Blazor component updates its state and re-renders.

Blazor supports calling JavaScript from .NET through IJSRuntime. It also supports calling .NET methods from JavaScript through mechanisms such as [JSInvokable] and a DotNetObjectReference.


The Demo Scenario

The demonstration uses two familiar Blazor pages:

  • The Counter page sends updates.

  • The Weather page receives updates.

The flow is:

  1. A user increments the counter.

  2. The Counter component updates its scoped state service.

  3. The new value is written to localStorage.

  4. The new value is broadcast through a named channel.

  5. Another tab receives the broadcast.

  6. JavaScript calls a method on the Weather component.

  7. The Weather component updates its displayed value.

  8. A newly opened tab restores the latest value from localStorage.

The implementation combines three types of state communication:

Scoped service
    In-tab Blazor state

BroadcastChannel
    Live cross-tab notification

localStorage
    Latest-value restoration

Each piece solves a different part of the problem.


Implementing the Demo

Step 1: Create the Shared Counter State Service

Create the following file:

Components/Services/CounterStateService.cs
namespace BroadcastChannelDemo.Components.Services;

public sealed class CounterStateService
{
    public event Action? CounterChanged;

    public int CurrentCount { get; private set; }

    public void Increment()
    {
        CurrentCount++;
        CounterChanged?.Invoke();
    }

    public void SetCount(int value)
    {
        if (CurrentCount == value)
        {
            return;
        }

        CurrentCount = value;
        CounterChanged?.Invoke();
    }
}

The service stores the counter value and raises an event whenever that value changes.

In a Blazor Server application, a scoped service normally lives for the lifetime of a Blazor circuit. Each independently opened browser tab usually establishes its own circuit, so the scoped service is useful for navigation and component communication within one tab, but it does not automatically synchronize multiple tabs.

That is why the Broadcast Channel API is still needed.


Step 2: Register the Service

Register the state service in Program.cs:

using BroadcastChannelDemo.Components;
using BroadcastChannelDemo.Components.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddScoped<CounterStateService>();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

The important line is:

builder.Services.AddScoped<CounterStateService>();

This makes one state service available throughout a user’s Blazor circuit.


Step 3: Create the JavaScript Module

Create:

wwwroot/js/broadcast-channel-demo.js
const activeChannels = new Map();

/**
 * Returns an existing channel or creates a new one.
 */
function getOrCreateChannel(channelName) {
    let channel = activeChannels.get(channelName);

    if (!channel) {
        channel = new BroadcastChannel(channelName);
        activeChannels.set(channelName, channel);
    }

    return channel;
}

/**
 * Broadcasts a message to other browser contexts that have
 * joined the same named channel.
 */
export function broadcastMessage(channelName, message) {
    const channel = getOrCreateChannel(channelName);
    channel.postMessage(message);
}

/**
 * Initializes a channel listener and forwards received messages
 * to an instance method on a Blazor component.
 */
export function initializeBroadcastChannel(channelName, dotNetHelper) {
    const channel = getOrCreateChannel(channelName);

    channel.onmessage = async event => {
        try {
            await dotNetHelper.invokeMethodAsync(
                "HandleBroadcastMessage",
                event.data
            );
        } catch (error) {
            console.error(
                `Unable to forward a message from '${channelName}' to .NET.`,
                error
            );
        }
    };
}

/**
 * Writes a value to localStorage.
 */
export function writeStorageValue(storageKey, value) {
    localStorage.setItem(storageKey, value);
}

/**
 * Reads a value from localStorage.
 */
export function readStorageValue(storageKey) {
    return localStorage.getItem(storageKey);
}

/**
 * Closes and removes a specific channel.
 */
export function disposeBroadcastChannel(channelName) {
    const channel = activeChannels.get(channelName);

    if (!channel) {
        return;
    }

    channel.close();
    activeChannels.delete(channelName);
}

This module provides five responsibilities:

  • Creating or retrieving a named channel

  • Sending messages

  • Receiving messages

  • Reading and writing browser storage

  • Closing channels during component cleanup

Using a JavaScript module keeps the browser-specific implementation separate from the Razor components.


Step 4: Define a Message Contract

For a very small demonstration, you could send only an integer:

channel.postMessage(5);

A typed message is more maintainable, especially as the application grows.

Create:

Components/Models/CounterBroadcastMessage.cs
namespace BroadcastChannelDemo.Components.Models;

public sealed record CounterBroadcastMessage(
    string Type,
    int Value,
    DateTimeOffset ChangedAt);

The equivalent JavaScript object will look like this:

{
    type: "counter-updated",
    value: 5,
    changedAt: "2026-07-04T18:30:00.000Z"
}

The Type property allows the channel to support additional events later.

For example:

counter-updated
user-preferences-updated
record-saved
session-ending
cache-invalidated

A channel name identifies the general message stream. The message type identifies the event within that stream.


Step 5: Update the Counter Page

The Counter page acts as the sender.

@page "/counter"
@rendermode InteractiveServer
@implements IAsyncDisposable

@using BroadcastChannelDemo.Components.Models
@using BroadcastChannelDemo.Components.Services
@using Microsoft.JSInterop

@inject CounterStateService CounterState
@inject IJSRuntime JS

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">
    Current count: <strong>@CounterState.CurrentCount</strong>
</p>

<button class="btn btn-primary" @onclick="IncrementCount">
    Increment
</button>

@code {
    private const string ChannelName = "blazor-counter-demo";
    private const string StorageKey = "blazor-counter-value";
    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
        {
            return;
        }

        module = await JS.InvokeAsync<IJSObjectReference>(
            "import",
            "./js/broadcast-channel-demo.js");
    }

    private async Task IncrementCount()
    {
        CounterState.Increment();

        if (module is null)
        {
            return;
        }

        var currentValue = CounterState.CurrentCount;

        await module.InvokeVoidAsync(
            "writeStorageValue",
            StorageKey,
            currentValue.ToString());

        var message = new CounterBroadcastMessage(
            Type: "counter-updated",
            Value: currentValue,
            ChangedAt: DateTimeOffset.UtcNow);

        await module.InvokeVoidAsync(
            "broadcastMessage",
            ChannelName,
            message);
    }

    public async ValueTask DisposeAsync()
    {
        if (module is null)
        {
            return;
        }

        try
        {
            await module.DisposeAsync();
        }
        catch (JSDisconnectedException)
        {
            // The Blazor circuit was already disconnected.
        }
    }
}

When the button is selected, the component:

  1. Updates the scoped service.

  2. Writes the latest count to localStorage.

  3. Creates a message containing the event type, value, and timestamp.

  4. Broadcasts that message to the other tabs.


Why Store the Value as Well as Broadcast It?

Broadcast channels deliver events. They are not a database and do not maintain a message history.

Consider this sequence:

  1. Tab A increments the counter to five.

  2. Tab A broadcasts the value.

  3. No other tab is open.

  4. The user opens Tab B.

  5. Tab B joins the channel.

Tab B does not receive the earlier message because it was not listening when the message was sent.

That is why the demo also uses localStorage.

The broadcast answers:

What changed just now?

The stored value answers:

What is the most recently known value?

This is a useful distinction between an event and state.


Step 6: Update the Weather Page

The Weather page acts as the receiver.

@page "/weather"
@rendermode InteractiveServer
@implements IAsyncDisposable

@using BroadcastChannelDemo.Components.Models
@using Microsoft.JSInterop

@inject IJSRuntime JS

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<div class="alert alert-info" role="status">
    <p>
        Latest counter value:
        <strong>@currentCounterValue</strong>
    </p>

    @if (!string.IsNullOrWhiteSpace(lastBroadcastMessage))
    {
        <p class="mb-0">@lastBroadcastMessage</p>
    }
</div>

@code {
    private const string ChannelName = "blazor-counter-demo";
    private const string StorageKey = "blazor-counter-value";

    private IJSObjectReference? module;
    private DotNetObjectReference<Weather>? dotNetReference;

    private int currentCounterValue;
    private string? lastBroadcastMessage;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
        {
            return;
        }

        module = await JS.InvokeAsync<IJSObjectReference>(
            "import",
            "./js/broadcast-channel-demo.js");

        dotNetReference = DotNetObjectReference.Create(this);

        await module.InvokeVoidAsync(
            "initializeBroadcastChannel",
            ChannelName,
            dotNetReference);

        var storedValue = await module.InvokeAsync<string?>(
            "readStorageValue",
            StorageKey);

        if (int.TryParse(storedValue, out var parsedValue))
        {
            currentCounterValue = parsedValue;
            lastBroadcastMessage =
                "Restored the latest value from browser storage.";

            await InvokeAsync(StateHasChanged);
        }
    }

    [JSInvokable]
    public async Task HandleBroadcastMessage(
        CounterBroadcastMessage message)
    {
        if (!string.Equals(
                message.Type,
                "counter-updated",
                StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        currentCounterValue = message.Value;
        lastBroadcastMessage =
            $"Received an update at {message.ChangedAt.LocalDateTime:T}.";

        await InvokeAsync(StateHasChanged);
    }

    public async ValueTask DisposeAsync()
    {
        dotNetReference?.Dispose();

        if (module is null)
        {
            return;
        }

        try
        {
            await module.InvokeVoidAsync(
                "disposeBroadcastChannel",
                ChannelName);

            await module.DisposeAsync();
        }
        catch (JSDisconnectedException)
        {
            // The browser or Blazor circuit was already disconnected.
        }
    }
}

Understanding the Receiver Lifecycle

The receiver performs its browser initialization in OnAfterRenderAsync.

That is important because browser APIs and JavaScript interoperability should not be invoked before the component has reached a browser-connected rendering phase.

On the first interactive render, the component:

  1. Imports the JavaScript module.

  2. Creates a DotNetObjectReference.

  3. Passes that reference to JavaScript.

  4. Joins the named broadcast channel.

  5. Registers a JavaScript message handler.

  6. Reads the most recent value from localStorage.

  7. Updates the UI.

When a broadcast is received, JavaScript executes:

await dotNetHelper.invokeMethodAsync(
    "HandleBroadcastMessage",
    event.data
);

Blazor finds the corresponding method because it is marked with:

[JSInvokable]

The component updates its C# fields and requests a render through:

await InvokeAsync(StateHasChanged);

Using InvokeAsync ensures that the update is dispatched through the component’s synchronization context instead of assuming that the JavaScript callback is already executing in the correct rendering context.


Step 7: Test the Application

Run the Blazor application and perform the following test.

Tab A

Open:

/counter

Tab B

Open:

/weather

Keep both tabs visible, or switch between them.

Click Increment in Tab A.

Tab B should display the new counter value without requiring:

  • A page refresh

  • A database query

  • A controller call

  • A custom SignalR hub

  • Server-side polling

Next, close Tab B.

Increment the counter several more times in Tab A.

Open the Weather page in a new tab. It should immediately restore the latest value from localStorage, even though the new tab did not receive the earlier broadcasts.


How the Complete Data Flow Works

Counter increment flow

User selects Increment
        |
        v
CounterStateService.Increment()
        |
        v
Counter component reads the new value
        |
        +-----------------------------+
        |                             |
        v                             v
Write to localStorage       Broadcast the update
                                      |
                                      v
                         Other same-origin tabs

Weather update flow

BroadcastChannel receives message
        |
        v
JavaScript message handler
        |
        v
dotNetHelper.invokeMethodAsync(...)
        |
        v
[JSInvokable] C# method
        |
        v
Update component state
        |
        v
StateHasChanged()
        |
        v
Updated Blazor UI

New-tab restoration flow

Weather page opens
        |
        v
Import JavaScript module
        |
        v
Join BroadcastChannel
        |
        v
Read localStorage
        |
        v
Display latest known value

When Should You Use the Broadcast Channel API?

The API works well when the communication is:

  • Between tabs or windows on the same browser

  • Between pages on the same origin

  • Local to a single user’s browser profile

  • Event-oriented

  • Relatively lightweight

  • Not dependent on guaranteed message delivery

Good use cases include the following.

Synchronizing user preferences

A user changes the application theme or display density in one tab. Other open tabs update immediately.

{
    type: "theme-changed",
    theme: "dark"
}

Notifying tabs that authentication changed

One tab signs out. Other tabs receive a notification and can navigate to the sign-in page or clear sensitive state.

Do not broadcast authentication tokens or secrets. Broadcast only the event that the authentication state changed.

{
    type: "user-signed-out"
}

Invalidating cached data

A user updates a record in one tab. Other tabs are told that their local view may be stale.

{
    type: "customer-updated",
    customerId: 451
}

The receiving tab can then decide whether to reload that record.

Coordinating dashboards

One page changes a filter, time range, or selected project. Another dashboard tab can react to the selection.

Preventing duplicate local work

Tabs can announce that one tab has started a browser-local operation, such as editing a document or refreshing an expensive client-side cache.

Coordinating multi-page workflows

A user may have a list page open in one tab and a detail page open in another. Saving the detail record can notify the list page to refresh.


When Should You Not Use It?

The Broadcast Channel API is useful, but it is not a replacement for every messaging technology.

Do not use it for communication between different users

Broadcast messages remain within compatible contexts in one browser environment.

They do not synchronize:

  • Two different users

  • Two different computers

  • A phone and a desktop

  • Different browser profiles

  • Different browser applications

Use a server-side solution such as SignalR when events must be delivered across devices or users.

Do not use it as permanent storage

A channel does not retain a durable history.

Use:

  • localStorage

  • sessionStorage

  • IndexedDB

  • An application database

  • A server-side cache

depending on the durability and scope you need.

Do not assume guaranteed delivery

A tab may be:

  • Closed

  • Suspended

  • Crashed

  • Not yet listening

  • Running under a different origin

Treat broadcasts as notifications, not as an authoritative transaction log.

Do not send secrets

Avoid broadcasting:

  • Access tokens

  • Refresh tokens

  • Passwords

  • Private keys

  • Sensitive personal data

  • Full authorization context

Every same-origin context able to join that channel may receive the message. Send the minimum information needed to describe the event.

Do not use it for large data transfers

Broadcast small event messages, identifiers, status changes, and cache-invalidation notices.

For large datasets, broadcast an event telling the receiving tab what changed, and let that tab obtain the authoritative data through the appropriate application service.


Broadcast Channel or SignalR?

A simple decision table helps clarify the difference.

RequirementBroadcast ChannelSignalR
Communicate between tabs in one browserYesYes
Communicate between different usersNoYes
Communicate between devicesNoYes
Requires server infrastructureNo additional infrastructureYes
Works across unrelated originsNoPotentially
Supports server-originated eventsNoYes
Durable deliveryNoNot automatically
Best for local tab coordinationYesUsually excessive
Best for multi-user real-time applicationsNoYes

Use Broadcast Channel when the browser itself is the communication boundary.

Use SignalR when the application server is the communication hub.

In some applications, using both is reasonable:

Server event
    |
    v
SignalR sends event to one active tab
    |
    v
That tab broadcasts a local notification
    |
    v
Other tabs update

However, this should be an intentional design decision. If every tab already has its own SignalR connection and must receive every server event independently, an additional broadcast layer may not be necessary.


Production Improvements

The demo is intentionally small. A production implementation should add a few safeguards.

Use versioned messages

Message contracts change over time. Add a version:

{
    version: 1,
    type: "counter-updated",
    payload: {
        value: 5
    }
}

The receiver can reject unsupported versions instead of misinterpreting the message.

Add a message identifier

{
    messageId: "be88a587-3455-46a2-9e9f-b1562bb52a65",
    type: "counter-updated",
    payload: {
        value: 5
    }
}

A receiver can use this identifier to avoid processing accidental duplicates.

Include a sender identifier

Each tab can generate a unique identifier:

const tabId = crypto.randomUUID();

Include it in every message:

{
    senderId: tabId,
    type: "counter-updated",
    payload: {
        value: 5
    }
}

This helps with diagnostics and allows a tab to ignore its own messages when needed.

Validate every incoming message

Do not assume that every received object has the expected structure.

function isCounterMessage(message) {
    return message !== null
        && typeof message === "object"
        && message.type === "counter-updated"
        && Number.isInteger(message.value);
}

Validation is especially important when several application features share a channel.

Avoid one global channel for everything

Instead of:

application

consider focused channel names:

application-auth
application-preferences
application-record-events

Focused channels reduce unrelated message handling and make the intent clearer.

Handle unsupported browsers

Feature detection is straightforward:

export function isBroadcastChannelSupported() {
    return "BroadcastChannel" in globalThis;
}

A production application can fall back to another mechanism when necessary.

Clean up JavaScript and .NET references

A DotNetObjectReference keeps a callable connection to the .NET object. Dispose it when the component is removed.

Also close the broadcast channel and release imported JavaScript modules.

This becomes especially important in long-running Blazor Server applications, where users may navigate among many components during one circuit.

Treat browser state as advisory

For business-critical information, the server or database should remain authoritative.

A broadcast can say:

Customer 451 changed.

The receiving tab should generally reload Customer 451 from the authoritative application service instead of trusting a complete customer object contained in the browser message.


A Reusable Event-Bus Design

Once the basic demonstration works, the JavaScript can be wrapped behind a reusable C# service.

For example:

public interface IBrowserBroadcastService
{
    ValueTask PublishAsync<TMessage>(
        string channelName,
        TMessage message);

    ValueTask<string?> ReadStorageAsync(string key);

    ValueTask WriteStorageAsync(
        string key,
        string value);
}

A component could then publish an event without knowing the JavaScript function names:

await BroadcastService.PublishAsync(
    "application-record-events",
    new RecordChangedMessage(
        RecordType: "Customer",
        RecordId: customerId,
        ChangeType: "Updated"));

This approach provides:

  • One location for channel naming rules

  • Centralized error handling

  • Consistent message envelopes

  • Easier unit testing of components

  • Less JavaScript interop code in Razor files

  • A natural place to add logging and browser feature detection

The Broadcast Channel API remains a browser technology, but it does not have to leak into every component.


The Most Important Design Lesson

The interesting part of this example is not the counter itself.

The important lesson is that different state mechanisms solve different state problems.

The scoped Blazor service provides:

State within one Blazor circuit

The Broadcast Channel API provides:

Live notifications among same-origin tabs

localStorage provides:

A latest-known browser value for tabs that open later

None of these mechanisms replaces the others.

Together, they create a lightweight cross-tab synchronization pattern without requiring a backend messaging system.


Conclusion

The Broadcast Channel API is one of those browser features that many developers never encounter until they need it.

Then it feels almost perfect for the problem.

With only a named channel, postMessage(), and a message listener, your application can coordinate multiple same-origin tabs without adding another server-side communication layer.

In a Blazor Server application, JavaScript interoperability bridges the browser API and your Razor components:

Blazor component
    ↕
JavaScript interoperability
    ↕
BroadcastChannel
    ↕
Other browser tabs

Combining that live event stream with localStorage gives you both immediate updates and restore-on-open behavior.

It is not a replacement for SignalR, durable storage, or a server-side event bus. It is a focused tool for a focused problem: allowing related pages in the same browser to communicate.

For tab-to-tab coordination, cache invalidation, preference synchronization, sign-out notifications, and multi-page workflows, that is not merely convenient.

It is pretty cool.


[source code]

Comments

Popular posts from this blog

Customizing PWA Manifest and Icons for a Polished User Experience 🚀

Offline-First Strategy with Blazor PWAs: A Complete Guide 🚀

Yes, Blazor Server can scale!