I feel like using the state management is an overkill in this scenario. I am willing to create the same functionality basically.
list of clickable items. you click on the item, then go back and brought to the exact same spot without page reload.
to me it looks like you could implement this by having 2 different sections on the same page
- section - list of items
- section - item itself
- some additional sections (editing forms, comments, etc.)
when traversing from 1 section to another we could use JS to push the new browser history state and then subscribe to state pop event. Here is the minimal working solution (I use the standard WASM .Net 5 template with counter page).
put this into index.html
<script>
var _instanceRef;
function setInstance(instanceRef) {
_instanceRef = instanceRef;
}
function setLocation(url) {
window.history.pushState('todo meta information', 'Title', url);
}
window.onpopstate = function (e) {
_instanceRef.invokeMethodAsync('GoBack');
};
</script>
and this is the modified counter.razor file
@page "/counter"
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
<h1>Counter</h1>
<button class="btn btn-primary @IsActive(isFeedActive)" id="pills-contact-tab" type="button" aria-selected="@isFeedActive" @onclick="NavigateToFeed">
Feed
</button>
<button class="btn btn-primary @IsActive(isFeedItemActive)" id="pills-contact-tab" type="button" aria-selected="@isFeedItemActive" @onclick="NavigateToItem">
Feed item
</button>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade @IsShowActive(isFeedActive)" id="pills-home" role="tabpanel" aria-labelledby="pills-home-tab">
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button></div>
<div class="tab-pane fade @IsShowActive(isFeedItemActive)" id="pills-profile" role="tabpanel" aria-labelledby="pills-profile-tab">
loadable item contents here.
</div>
</div>
@code {
public bool isFeedActive = true;
public bool isFeedItemActive = false;
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
protected override async Task OnInitializedAsync()
{
await JSRuntime.InvokeVoidAsync("setInstance", DotNetObjectReference.Create(this));
}
public async Task NavigateToFeed()
{
isFeedActive = true;
isFeedItemActive = false;
await JSRuntime.InvokeVoidAsync("setLocation", $"/counter");
}
public async Task NavigateToItem()
{
isFeedActive = false;
isFeedItemActive = true;
await JSRuntime.InvokeVoidAsync("setLocation", $"/counter/item/56");
}
[JSInvokable]
public async Task GoBack()
{
isFeedActive = true;
isFeedItemActive = false;
StateHasChanged();
}
private string IsActive(bool check)
{
return check ? "active" : "";
}
private string IsShowActive(bool check)
{
return check ? "show active" : "";
}
}
steps are
- navigate to counter page
- click on count button couple of times to imitate the state change
- navigate to second tab (see that URL is changed but page not refreshed)
- click on back button and witness the same state as in step 2.

note that I am also using bootstrap 5 pills in this example. Follow the official website for latest CSS and JS links from CDN and place them into index.html
This solution requires some (a complete) rework. Adding validations etc. Also instance should be disposed so probably JS ES6 module shall be used instead (google JS isolation in Blazor). But it's only PoC at this point so please don't throw rocks at me :)
edit:
also please note that window.history.pushState is probably HTML5 only function (double check this).
Also, the page that you are "locating to" should be reachable after you refresh the page! i.e. if you open the "single item" section at location "/counter/pagethatdoesnotexists" and user refreshes the page - the user will be presented with page not found error. This is not really an issue if you back your logic up with pages that actually exist.
I have just figured that this solution will only work when user goes back in time 1 time. If user clicks forward after he clicked backward button - this will trigger the page loading and the state will be lost.. Still better than nothing, but if anybody has any improvement suggestions I would like to know them :)
edit2 sorry I think I have overly complicated this task. We could simply use optional route parameters to set the item id.
url
@page "/counter2/{Id:long?}"
and the rest of the code
[Parameter]
public long? Id { get; set; }
protected override async Task OnParametersSetAsync()
{
if (Id != null)
{
isFeedActive = false;
isFeedItemActive = true;
}
else
{
isFeedActive = true;
isFeedItemActive = false;
}
}
public async Task NavigateToFeed()
{
NavigationManager.NavigateTo("counter2");
}
public async Task NavigateToItem()
{
NavigationManager.NavigateTo("counter2/5");
}
this way the page reload is not happening (i.e. you keep the state), same as in solution 1. ALSO, if user clicks on forward button in browser - same page will be loaded. When user opens the link directly, while no "item list" content is loaded, you might want to exclude initial page contents loading depending on your requirements. Alternatively redirect user to dedicated item details page in this case.