Sharing data between browsing contexts

Sometimes, a user logged out of your website but in the other browser tabs, the user is still logged in and create an inconsistency between browser tabs. That is when you need to share data between browsing context to avoid any unwanted inconsistency. In this tutorial, you will learn:

  • What is a browsing context?
  • Why do you need to sync data between browsing contexts?
  • What is a browsing context broadcaster?
  • Implement a browsing context broadcaster.
  • Component broadcaster or global broadcaster?
  • The limitation of BroadcastChannel.
  • Best practices.
You can download the example code used in this topic on GitHub.

What is a browsing context?

According to MDN Web Docs, a browsing context is the environment in which a browser displays a Document. To make it simple to understand, a browsing context is a browser tab, a browser window, frame, or an iframe element. The following video demonstrates sharing data between browsing contexts:


Why do you need to sync data between browsing contexts?

  • Avoid unwanted inconsistency: Your website might have many states, logged in and logged out, for example. When a user open many browser tabs, and they logged out in a tab, but the other tabs are still logged in. It will create a confusion for the user and might lead to some security issues. Furthermore, you don't want the users to think this is a bug rather than the inconsistency in your website.
  • Reduce necessary calls to the server: Imaging a user is changing their profile name and avatar, which you are displaying it in the menu bar. Instead of sending a request to the server, you can tell the browser to take the profile name and the avatar from the other tab. Saving you at least 1 request per tab per user. In a large scale, it will improve the performance a lot.

What is a browsing context broadcaster?

A browsing context broadcaster is an implementation of publish/subscribe design pattern where a browsing context publishing a message to a channel and all the channel's subscribers will receive it and act accordingly. We will use the Broadcast Channel API as the underlying technique. The following image illustrates how a browsing context broadcaster works:

browsing-context-broadcaster.png


Implement a browsing context broadcaster

As we mentioned before, we will use the Broadcast Channel API. It has the BroadcastChannel class, and we are going to use this class to implement a browsing context broadcaster.

  1. Create a JavaScript file with a method to publish a message and a method to subscribe to a channel. For example, we will create the BlazorSchoolBroadcast (wwwroot/js/BlazorSchoolBroadcast.js) JavaScript module as follows:
export function postMessage(channelName, message)
{
    let channel = new BroadcastChannel(channelName);
    channel.postMessage(JSON.stringify(message));
}

export function listenChannel(channelName, csharpBroadcaster)
{
    let channel = new BroadcastChannel(channelName);
    channel.onmessage = (event) =>
    {
        csharpBroadcaster.invokeMethodAsync("Notify", JSON.parse(event.data));
    }
}
You can read more about JavaScript module in the Add JavaScript to Blazor tutorial.
  1. Create a broadcast data record to store the data. For example:
public record BroadcastData<T>(JsonElement Data)
{
    public T? GetData()
    {
        return Data.Deserialize<T>(new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
    }
}
  1. Create the browsing context broadcaster base class to interact with the JavaScript module. For example:
public abstract class BlazorSchoolBroadcasterBase<T> : IAsyncDisposable
{
    private readonly IJSRuntime _jsRuntime;
    private Lazy<IJSObjectReference> _blazorSchoolBroadcastJsRef = new();

    public abstract string ChannelName { get; }
    public event EventHandler<BroadcastData<T>> OnMessageReceived = (sender, args) => { };

    public BlazorSchoolBroadcasterBase(IJSRuntime jsRuntime)
    {
        _jsRuntime = jsRuntime;
    }

    public async Task SendMessageAsync(T message)
    {
        await WaitForReference();
        await _blazorSchoolBroadcastJsRef.Value.InvokeVoidAsync("postMessage", ChannelName, message);
    }

    public async Task ListenChannelAsync()
    {
        await WaitForReference();
        var wrappedInstance = DotNetObjectReference.Create(this);
        await _blazorSchoolBroadcastJsRef.Value.InvokeVoidAsync("listenChannel", ChannelName, wrappedInstance);
    }

    [JSInvokable]
    public void Notify(JsonElement jsonData)
    {
        OnMessageReceived?.Invoke(this, new(jsonData));
    }

    protected async Task WaitForReference()
    {
        if (_blazorSchoolBroadcastJsRef.IsValueCreated is false)
        {
            _blazorSchoolBroadcastJsRef = new(await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "/js/BlazorSchoolBroadcast.js"));
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (_blazorSchoolBroadcastJsRef.IsValueCreated)
        {
            await _blazorSchoolBroadcastJsRef.Value.DisposeAsync();
        }
    }
}
In the above example, T is the data you want to share between your component. It can be a primitive data like string, int or a complex data like ExampleClass.

Component broadcaster

This type of broadcaster allows you to subscribe to a channel only when you need it. To access the data, your components need to subscribe to the broadcaster first. When the component receives a notification, you need to act accordingly.

  1. Create a new broadcaster class that extends the base class we have created at step 3. For example:
public class BlazorSchoolComponentBroadcaster1 : BlazorSchoolBroadcasterBase<string>
{
    public BlazorSchoolComponentBroadcaster1(IJSRuntime jsRuntime) : base(jsRuntime)
    {
    }

    public override string ChannelName { get; } = "BlazorSchoolComponentBroadcaster1";
}
  1. Register the component broadcaster class at Program.cs. For example:
builder.Services.AddScoped<BlazorSchoolComponentBroadcaster1>();

Global broadcaster

This type of broadcaster allows you to access the data anywhere without subscribe to the broadcaster.

  1. Create a new broadcaster class that extends the base class we have created at step 3. For example:
public class BlazorSchoolGlobalBroadcaster : BlazorSchoolBroadcasterBase<ExampleClass>
{
    public override string ChannelName { get; } = "BlazorSchoolGlobalBroadcaster";

    public BlazorSchoolGlobalBroadcaster(IJSRuntime jsRuntime) : base(jsRuntime)
    {
    }
}
  1. Create a cascading component to pass the data (T in the BlazorSchoolBroadcasterBase<T>). For example, our BlazorSchoolCascadingGlobalBroadcastData.razor:
@implements IDisposable
@inject BlazorSchoolGlobalBroadcaster BlazorSchoolGlobalBroadcaster

<CascadingValue TValue="BroadcastData<ExampleClass>" Value="_currentData" ChildContent="ChildContent" Name="BlazorSchoolCascadingGlobalBroadcastData" />

@code {
    private BroadcastData<ExampleClass> _currentData = default!;

    [Parameter]
    public RenderFragment ChildContent { get; set; } = _ => { };

    protected override async Task OnInitializedAsync()
    {
        BlazorSchoolGlobalBroadcaster.OnMessageReceived += OnGlobalBroadcasterNotify;
        await BlazorSchoolGlobalBroadcaster.ListenChannelAsync();
    }

    public void OnGlobalBroadcasterNotify(object? sender, BroadcastData<ExampleClass> broadcastData)
    {
        _currentData = broadcastData;
        StateHasChanged();
    }

    public void Dispose()
    {
        BlazorSchoolGlobalBroadcaster.OnMessageReceived -= OnGlobalBroadcasterNotify;
    }
}
  1. Register the global broadcaster class at Program.cs. For example:
builder.Services.AddScoped<BlazorSchoolGlobalBroadcaster>();
  1. Register the cascading component which we have created at step 5 at App.razor. For example:
<BlazorSchoolCascadingGlobalBroadcastData>
    <Router AppAssembly="@typeof(App).Assembly">
        ...
    </Router>
</BlazorSchoolCascadingGlobalBroadcastData>
  1. Create the global broadcaster data accessor class. For example:
public class BlazorSchoolGlobalBroadcasterAccessor : ComponentBase
{
    [CascadingParameter(Name = "BlazorSchoolCascadingGlobalBroadcastData")]
    public BroadcastData<ExampleClass> CurrentData { get; set; } = default!;

    [Parameter]
    public RenderFragment<BroadcastData<ExampleClass>>? ChildContent { get; set; }

    protected override void BuildRenderTree(RenderTreeBuilder builder) => builder.AddContent(0, ChildContent?.Invoke(CurrentData));
}

Using the component broadcaster

  • To publish a message, inject the broadcaster class into the component. Then use the SendMessageAsync method to publish a message. For example:
@inject BlazorSchoolComponentBroadcaster1 BlazorSchoolComponentBroadcaster1

<form>
    <label>
        Enter your name:
        <input type="text" @bind-value="ExampleString" />
    </label>
    <button type="button" @onclick="SendDataBroadcaster1Async">Sync other browsing contexts</button>
</form>

@code {
    public string ExampleString { get; set; } = "Blazor School";

    private async Task SendDataBroadcaster1Async()
    {
        await BlazorSchoolComponentBroadcaster1.SendMessageAsync(ExampleString);
    }
}
  • To access the data, inject the broadcaster class into the component. Then call the ListenChannelAsync at the Initialize phase (see Component Lifecycle for more information). Next, subscribe for the OnMessageReceived event and act accordingly. For example:
@inject BlazorSchoolComponentBroadcaster1 BlazorSchoolComponentBroadcaster1
@implements IDisposable

<div>The data is @ReceivedData</div>

@code {
    public string ReceivedData { get; set; } = "Blazor School";

    protected override async Task OnInitializedAsync()
    {
        BlazorSchoolComponentBroadcaster1.OnMessageReceived += OnBroadcaster1Notify;
        await BlazorSchoolComponentBroadcaster1.ListenChannelAsync();
    }

    private void OnBroadcaster1Notify(object? sender, BroadcastData<string> broadcastData)
    {
        ReceivedData = broadcastData.GetData() ?? "";
        StateHasChanged();
    }

    public void Dispose()
    {
        BlazorSchoolComponentBroadcaster1.OnMessageReceived -= OnBroadcaster1Notify;
    }
}

Using the global broadcaster

  • To publish a message, inject the broadcaster class into the component. Then use the SendMessageAsync method to publish a message. For example:
@inject BlazorSchoolGlobalBroadcaster BlazorSchoolGlobalBroadcaster

<EditForm Model="FormModel">
    @if (FormModel.LoggedIn)
    {
        <label>
            Enter your name:
            <InputText @bind-Value="FormModel.Username" />
        </label>
        <div>
            <button type="button" @onclick="SaveChangesAsync">Save changes</button>
            <button type="button" @onclick="LogoutAsync">Log out</button>
        </div>
    }
    else
    {
        <button type="button" @onclick="LoginAsync">Log in</button>
    }
</EditForm>

@code {
    public ExampleClass FormModel { get; set; } = new();

    private async Task SaveChangesAsync()
    {
        await BlazorSchoolGlobalBroadcaster.SendMessageAsync(FormModel);
    }

    private async Task LoginAsync()
    {
        FormModel.LoggedIn = true;
        await SaveChangesAsync();
    }

    private async Task LogoutAsync()
    {
        FormModel = new();
        await SaveChangesAsync();
    }
}
  • To access the data in the UI, use the cascading component you have created at step 5. For example:
<BlazorSchoolGlobalBroadcasterAccessor Context="broadcastData">
    <a href="https://blazorschool.com" target="_blank">@broadcastData?.GetData()?.Username</a>
</BlazorSchoolGlobalBroadcasterAccessor>
  • To access the data in the code, subscribe to the OnMessageReceived event of the global broadcaster. For example:
@inject BlazorSchoolGlobalBroadcaster BlazorSchoolGlobalBroadcaster
@implements IDisposable

...

@code {
    public ExampleClass FormModel { get; set; } = new();

    protected override void OnInitialized()
    {
        BlazorSchoolGlobalBroadcaster.OnMessageReceived += OnGlobalBroadcasterUpdate;
    }

    private void OnGlobalBroadcasterUpdate(object? sender, BroadcastData<ExampleClass> broadcastData)
    {
        FormModel = broadcastData.GetData() ?? new();
        StateHasChanged();
    }

    public void Dispose()
    {
        BlazorSchoolGlobalBroadcaster.OnMessageReceived -= OnGlobalBroadcasterUpdate;
    }
}
  • Another way to access the data is to use the CascadingParameter attribute with the name in the cascading component you have created at step 5. For example:
[CascadingParameter(Name = "BlazorSchoolCascadingGlobalBroadcastData")]
public BroadcastData<ExampleClass> CurrentData { get; set; } = default!;

Component broadcaster or global broadcaster?

Component broadcaster is suitable for data that does not appear on every pages of your website. For example: the cart information is only useful in the checkout page or product page and not useful in the register or forgot password page. In this case, a component broadcaster is preferable.

Global broadcaster is suitable for data that appears on every or almost every pages. For example, the profile name displayed at the menu bar. In this case, a global broadcaster is preferable.


The limitation of BroadcastChannel

BroadcastChannel only works with the same origin (including protocol, host and port) and on a same browser (incognito/private count as different browser). The following image illustrates this characteristic of BroadcastChannel:

broadcastchannel-limitation.png


Best practices

  • Use the preferable broadcaster. Based on what you need, use component broadcaster or global broadcaster accordingly.
  • A broadcaster should deal with only 1 type of data. When publishing you are publishing a string then you should expect to receive a string.
  • The ChannelName should have a clear meaning.
BLAZOR SCHOOL
Designed and built with care by our dedicated team, with contributions from a supportive community. We strive to provide the best learning experience for our users.
Docs licensed CC-BY-SA-4.0
Copyright © 2021-2025 Blazor School
An unhandled error has occurred. Reload 🗙