aspnetcore/blazor/components/component-disposal.md
This article explains the ASP.NET Core Razor component disposal process with xref:System.IDisposable and xref:System.IAsyncDisposable.
If a component implements xref:System.IDisposable or xref:System.IAsyncDisposable, the framework calls for resource disposal when the component is removed from the UI. Don't rely on the exact timing of when these methods are executed. For example, xref:System.IAsyncDisposable can be triggered before or after an asynchronous xref:System.Threading.Tasks.Task awaited in OnInitalizedAsync or OnParametersSetAsync is called or completes. Also, object disposal code shouldn't assume that objects created during initialization or other lifecycle methods exist.
Components shouldn't need to implement xref:System.IDisposable and xref:System.IAsyncDisposable simultaneously. If both are implemented, the framework only executes the asynchronous overload.
Developer code must ensure that xref:System.IAsyncDisposable implementations don't take a long time to complete.
For more information, see the introductory remarks of xref:blazor/components/sync-context.
Examples throughout the JavaScript (JS) interop articles demonstrate typical object disposal patterns:
When calling JS from .NET, as described in xref:blazor/js-interop/call-javascript-from-dotnet, dispose any created xref:Microsoft.JSInterop.IJSObjectReference/xref:Microsoft.JSInterop.IJSInProcessObjectReference/xref:Microsoft.JSInterop.Implementation.JSObjectReference either from .NET or from JS to avoid leaking JS memory.
When calling .NET from JS, as described in xref:blazor/js-interop/call-dotnet-from-javascript, dispose any created xref:Microsoft.JSInterop.DotNetObjectReference either from .NET or from JS to avoid leaking .NET memory.
JS interop object references are implemented as a map keyed by an identifier on the side of the JS interop call that creates the reference. When object disposal is initiated from either the .NET or JS side, Blazor removes the entry from the map, and the object can be garbage collected as long as no other strong reference to the object is present.
At a minimum, always dispose objects created on the .NET side to avoid leaking .NET managed memory.
For more information, see xref:blazor/js-interop/index#dom-cleanup-tasks-during-component-disposal.
For guidance on xref:Microsoft.JSInterop.JSDisconnectedException when a circuit is disconnected, see xref:blazor/js-interop/index#javascript-interop-calls-without-a-circuit. For general JavaScript interop error handling guidance, see the JavaScript interop section in xref:blazor/fundamentals/handle-errors#javascript-interop.
IDisposableFor synchronous disposal tasks, use xref:System.IDisposable.Dispose%2A?displayProperty=nameWithType.
The following component:
@implements Razor directive.obj, which is a type that implements xref:System.IDisposable.obj is created in a lifecycle method (not shown).@implements IDisposable
...
@code {
...
public void Dispose()
{
obj?.Dispose();
}
}
If a single object requires disposal, a lambda can be used to dispose of the object when xref:System.IDisposable.Dispose%2A is called. The following example appears in the xref:blazor/components/rendering#receiving-a-call-from-something-external-to-the-blazor-rendering-and-event-handling-system article and demonstrates the use of a lambda expression for the disposal of a xref:System.Timers.Timer.
:::moniker range=">= aspnetcore-9.0"
TimerDisposal1.razor:
:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal1.razor":::
:::moniker-end
:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0"
TimerDisposal1.razor:
:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal1.razor":::
:::moniker-end
:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"
CounterWithTimerDisposal1.razor:
:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor":::
:::moniker-end
:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0"
CounterWithTimerDisposal1.razor:
:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor":::
:::moniker-end
:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0"
CounterWithTimerDisposal1.razor:
:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor":::
:::moniker-end
:::moniker range="< aspnetcore-5.0"
CounterWithTimerDisposal1.razor:
:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor":::
:::moniker-end
[!NOTE] In the preceding example, the call to xref:Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged%2A is wrapped by a call to xref:Microsoft.AspNetCore.Components.ComponentBase.InvokeAsync%2A?displayProperty=nameWithType because the callback is invoked outside of Blazor's synchronization context. For more information, see xref:blazor/components/rendering#receiving-a-call-from-something-external-to-the-blazor-rendering-and-event-handling-system.
If the object is created in a lifecycle method, such as OnInitialized{Async}, check for null before calling Dispose.
:::moniker range=">= aspnetcore-9.0"
TimerDisposal2.razor:
:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal2.razor":::
:::moniker-end
:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0"
TimerDisposal2.razor:
:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal2.razor":::
:::moniker-end
:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"
CounterWithTimerDisposal2.razor:
:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor":::
:::moniker-end
:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0"
CounterWithTimerDisposal2.razor:
:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor":::
:::moniker-end
:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0"
CounterWithTimerDisposal2.razor:
:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor":::
:::moniker-end
:::moniker range="< aspnetcore-5.0"
CounterWithTimerDisposal2.razor:
:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor":::
:::moniker-end
For more information, see:
IAsyncDisposableFor asynchronous disposal tasks, use xref:System.IAsyncDisposable.DisposeAsync%2A?displayProperty=nameWithType.
The following component:
@implements Razor directive.obj, which is an unmanaged type that implements xref:System.IAsyncDisposable.obj is created in a lifecycle method (not shown).@implements IAsyncDisposable
...
@code {
...
public async ValueTask DisposeAsync()
{
if (obj is not null)
{
await obj.DisposeAsync();
}
}
}
For more information, see:
null to disposed objectsUsually, there's no need to assign null to disposed objects after calling xref:System.IDisposable.Dispose%2A/xref:System.IAsyncDisposable.DisposeAsync%2A. Rare cases for assigning null include the following:
null after disposal to gracefully skip further calls to xref:System.IDisposable.Dispose%2A/xref:System.IAsyncDisposable.DisposeAsync%2A.null allows the garbage collector to free the object in spite of the long-lived process holding a reference to it.These are unusual scenarios. For objects that are implemented correctly and behave normally, there's no point in assigning null to disposed objects. In the rare cases where an object must be assigned null, we recommend documenting the reason and seeking a solution that prevents the need to assign null.
StateHasChanged[!NOTE] Calling xref:Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged%2A in
DisposeandDisposeAsyncisn't supported. xref:Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged%2A might be invoked as part of tearing down the renderer, so requesting UI updates at that point isn't supported.
Always unsubscribe event handlers from .NET events. The following Blazor form examples show how to unsubscribe an event handler in the Dispose method.
Private field and lambda approach:
@implements IDisposable
<EditForm ... EditContext="editContext" ...>
...
<button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>
@code {
...
private EventHandler<FieldChangedEventArgs>? fieldChanged;
protected override void OnInitialized()
{
editContext = new(model);
fieldChanged = (_, __) =>
{
...
};
editContext.OnFieldChanged += fieldChanged;
}
public void Dispose()
{
editContext.OnFieldChanged -= fieldChanged;
}
}
Private method approach:
@implements IDisposable
<EditForm ... EditContext="editContext" ...>
...
<button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>
@code {
...
protected override void OnInitialized()
{
editContext = new(model);
editContext.OnFieldChanged += HandleFieldChanged;
}
private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
{
...
}
public void Dispose()
{
editContext.OnFieldChanged -= HandleFieldChanged;
}
}
For more information on the xref:Microsoft.AspNetCore.Components.Forms.EditForm component and forms, see xref:blazor/forms/index and the other forms articles in the Forms node.
When anonymous functions, methods, or expressions, are used, it isn't necessary to implement xref:System.IDisposable and unsubscribe delegates. However, failing to unsubscribe a delegate is a problem when the object exposing the event outlives the lifetime of the component registering the delegate. When this occurs, a memory leak results because the registered delegate keeps the original object alive. Therefore, only use the following approaches when you know that the event delegate disposes quickly. When in doubt about the lifetime of objects that require disposal, subscribe a delegate method and properly dispose the delegate as the earlier examples show.
Anonymous lambda method approach (explicit disposal not required):
private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
{
formInvalid = !editContext.Validate();
StateHasChanged();
}
protected override void OnInitialized()
{
editContext = new(starship);
editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e);
}
Anonymous lambda expression approach (explicit disposal not required):
private ValidationMessageStore? messageStore;
[CascadingParameter]
private EditContext? CurrentEditContext { get; set; }
protected override void OnInitialized()
{
...
messageStore = new(CurrentEditContext);
CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
CurrentEditContext.OnFieldChanged += (s, e) =>
messageStore.Clear(e.FieldIdentifier);
}
The full example of the preceding code with anonymous lambda expressions appears in the xref:blazor/forms/validation#validator-components article.
For more information, see Cleaning up unmanaged resources and the topics that follow it on implementing the Dispose and DisposeAsync methods.
Trap xref:Microsoft.JSInterop.JSDisconnectedException in potential cases where loss of Blazor's SignalR circuit prevents JS interop calls and results an unhandled exception.
For more information, see the following resources: