JavaScript interop calls cannot be issued at this time. An interactive
component called IJSRuntime from OnInitializedAsync, and that
method also runs during prerendering, on the server, before any browser is attached. Move
the call into OnAfterRenderAsync behind a firstRender check.
If the result changes what the component shows, call StateHasChanged() once
after the call. The other way out is to switch prerendering off for that one component,
which has a cost described below.
The error
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.
at Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime.BeginInvokeJS(JSInvocationInfo& invocationInfo)
at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, JSCallType callType, CancellationToken cancellationToken, Object[] args)
at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, JSCallType callType, Object[] args)
at FixLab.Components.Pages.JsPrerender.OnInitializedAsync() in ...\FixLab\Components\Pages\JsPrerender.razor:line 14
The component that threw reads a value from the browser's local storage:
protected override async Task OnInitializedAsync()
{
lastPatient = await JS.InvokeAsync<string?>("localStorage.getItem", "lastPatient");
}
Why it happens
With @rendermode InteractiveServer, prerendering is on by default, so the
component runs twice. First, during the HTTP request, Blazor renders it to plain HTML so the
page arrives with content. Then blazor.web.js opens the circuit and a new
instance of the component runs again, live. OnInitializedAsync runs in both
passes. In the first there is no browser connection yet, so the JavaScript runtime
(RemoteJSRuntime in the trace) refuses the call. The exception escapes the
render and the whole response fails with a 500 before any HTML is sent.
OnAfterRenderAsync never runs during prerendering. It runs only in the live
pass, after the browser has applied the render, which is exactly when JavaScript can be
reached.
The fix
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
lastPatient = await JS.InvokeAsync<string?>("localStorage.getItem", "lastPatient");
StateHasChanged();
}
}
Two details matter. The firstRender check keeps it to one call: this method
runs after every render, and without the check the StateHasChanged() would
trigger another render, which calls this method again. And StateHasChanged() itself
is needed because the render has already happened. Without it, local storage held "Maria
Garcia" and the page still said "Last patient: none yet"; with it, the page showed "Last
patient: Maria Garcia".
The second-best option keeps the call in OnInitializedAsync and turns
prerendering off for this component with
@rendermode @(new InteractiveServerRenderMode(prerender: false)). That worked
too, but the component then sends no HTML at all until the circuit connects: a blank spot on
first load, and nothing for search engines to read.
How it was reproduced
A fresh dotnet new blazor -o FixLab --interactivity Server project on .NET SDK
10.0.401 (ASP.NET Core runtime 10.0.12, no extra packages). One page with
@rendermode InteractiveServer and @inject IJSRuntime JS called
localStorage.getItem in OnInitializedAsync; a plain GET of the
page returned 500 with the error above, before any browser was involved. The fixed page was
then opened in a browser with a value stored under lastPatient, and showed it.
Frequently asked
- Why can't I call JavaScript in OnInitializedAsync in Blazor?
- With prerendering on, OnInitializedAsync first runs on the server while the page HTML is being generated, before any browser is connected, so there is nothing to call. OnAfterRenderAsync runs only once the component is live in the browser.
- Do I need StateHasChanged after JS interop in OnAfterRenderAsync?
- Yes, when the result changes what the component shows. The render has already happened by the time OnAfterRenderAsync runs, so call StateHasChanged once, inside the firstRender check, to display the new value.
- How do I disable prerendering for one Blazor component?
- Give it the render mode new InteractiveServerRenderMode(prerender: false) instead of InteractiveServer. JavaScript interop in OnInitializedAsync then works, but the component renders nothing until the circuit connects, and crawlers see no content for it.
More decoded errors in the Fixes category. What runs when, and why, is in the component lifecycle part of the Blazor series.