State, Lifecycle and JS Interop
Lifecycle methods, StateHasChanged, and IJSRuntime for the cases where only JavaScript can do the job.
protected override async Task OnAfterRenderAsync(bool firstRender)A component instance has a lifetime, and Blazor calls methods on it at fixed points. Override the ones you need.
The lifecycle
OnInitialized and OnInitializedAsync run once, when the instance is created. OnParametersSet and OnParametersSetAsync run after that and again every time the parent supplies parameters. OnAfterRender(bool firstRender) and OnAfterRenderAsync run after the browser has the HTML, and are not called at all during static server rendering or prerendering.
Holding state and repainting
State is just fields and properties on the component. Blazor re-renders automatically after an event handler it invoked, and after an awaited task inside one completes. It cannot see changes from anywhere else, such as a timer or an event raised by a shared service. For those, call StateHasChanged(), which asks for a re-render. If the change arrives on another thread, wrap it in InvokeAsync so it runs on the component's own dispatcher first.
Calling JavaScript
Some things have no C# equivalent: focusing an element, reading the clipboard, driving a mapping library. Inject IJSRuntime and call InvokeVoidAsync for no result, or InvokeAsync<T> for one. Load your own code as an ES module by importing it into an IJSObjectReference, and dispose that reference when the component goes away.
What trips people up
JavaScript cannot be called before the first render. During prerendering there is no browser attached yet, so the call fails.
Example
* Components/Pages/CopyCode.razor *
"/copy-code"
InteractiveServer
IAsyncDisposable
IJSRuntime JS * IJSRuntime is registered by the framework *
<p></p>
<button ="CopyAsync" disabled="@(_module is null)">Copy the code</button>
{
private IJSObjectReference? _module;
private string _status = "Loading.";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Guard on firstRender: this method runs after every render,
// and importing the module again each time would leak references.
if (firstRender)
{
_module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./js/clipboard.js");
_status = "Ready.";
// Nothing told Blazor the fields changed, so ask for a repaint.
StateHasChanged();
}
}
private async Task CopyAsync()
{
await _module!.InvokeVoidAsync("copyText", "ABC-123");
_status = "Copied.";
}
// Dropping the reference without disposing it keeps the JS object alive.
public async ValueTask DisposeAsync()
{
if (_module is not null)
{
await _module.DisposeAsync();
}
}
}When to use it
- Load data for a details page that must reload when the route parameter changes.
- Push a notification from a background service into a component that is already on screen.
- Call a charting or mapping JavaScript library that has no .NET wrapper.
More examples
The JavaScript module the example imports
Importing a module, rather than hanging functions off the global window object, keeps the names scoped to the component that uses them. Call elementWidth from C# with await module.InvokeAsync<double>("elementWidth", "chart"). Anything a JS function returns must be JSON-serializable, so a DOM element has to come back as an IJSObjectReference instead.
// wwwroot/js/clipboard.js
// An ES module: every exported function is callable from C# by name.
export function copyText(text) {
return navigator.clipboard.writeText(text);
}
// Returning a value works too, as long as it can be turned into JSON.
export function elementWidth(id) {
const el = document.getElementById(id);
return el ? el.getBoundingClientRect().width : 0;
}Reloading when a parameter changes, and repainting from elsewhere
This is the shape of almost every live screen: load on parameter change, subscribe to a shared service, repaint through InvokeAsync, unsubscribe on dispose. The missing Dispose is the most common memory leak in interactive server apps, because the service holds a reference to a component the user left ten minutes ago.
@* Components/Pages/Product.razor *@
@page "/product/{Id:int}"
@rendermode InteractiveServer
@implements IDisposable
@inject PriceFeed Feed
<h1>@_name</h1>
<p>Price: @_price</p>
@code {
[Parameter]
public int Id { get; set; }
private string? _name;
private decimal _price;
protected override void OnInitialized() => Feed.PriceChanged += OnPriceChanged;
// Runs on every parameter change, so /product/2 reloads. OnInitializedAsync
// would not: the router reuses this instance for the new route.
protected override async Task OnParametersSetAsync()
{
var product = await Feed.GetAsync(Id);
_name = product.Name;
_price = product.Price;
}
private void OnPriceChanged(int id, decimal price)
{
if (id != Id)
{
return;
}
// The event may arrive on another thread. InvokeAsync moves the work
// onto this component's dispatcher before touching its state.
_ = InvokeAsync(() =>
{
_price = price;
StateHasChanged();
});
}
// Not unsubscribing keeps this component, and its state, alive forever.
public void Dispose() => Feed.PriceChanged -= OnPriceChanged;
}Keeping prerendered data instead of fetching it twice
A prerendered interactive component runs OnInitializedAsync twice: once to produce the HTML, once when it comes alive. Without persisted state that means two database queries and a visible flicker. Before .NET 10 this needed the PersistentComponentState service and a registered callback; the attribute replaced that for the common case, and the property must be public.
@* Components/Pages/Catalog.razor *@
@page "/catalog"
@rendermode InteractiveServer
@inject CatalogService Catalog
@if (Items is null)
{
<p>Loading.</p>
}
else
{
<ul>
@foreach (var item in Items)
{
<li>@item</li>
}
</ul>
}
@code {
// [PersistentState] is .NET 10. The value is serialized into the
// prerendered page and handed back when the component becomes
// interactive, so the query below does not run a second time.
[PersistentState]
public List<string>? Items { get; set; }
protected override async Task OnInitializedAsync()
{
Items ??= await Catalog.GetNamesAsync();
}
}
Discussion