|
| 1 | +--- |
| 2 | +title: ASP.NET Core Razor component disposal |
| 3 | +author: guardrex |
| 4 | +description: Learn about ASP.NET Core Razor component component disposal with IDisposable and IAsyncDisposable. |
| 5 | +monikerRange: '>= aspnetcore-3.1' |
| 6 | +ms.author: riande |
| 7 | +ms.custom: mvc |
| 8 | +ms.date: 03/18/2025 |
| 9 | +uid: blazor/components/component-disposal |
| 10 | +--- |
| 11 | +# ASP.NET Core Razor component disposal |
| 12 | + |
| 13 | +[!INCLUDE[](~/includes/not-latest-version.md)] |
| 14 | + |
| 15 | +This article explains the ASP.NET Core Razor component disposal with with <xref:System.IDisposable> and <xref:System.IAsyncDisposable>. |
| 16 | + |
| 17 | +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`](xref:blazor/components/lifecycle#component-initialization-oninitializedasync) or [`OnParametersSetAsync`](xref:blazor/components/lifecycle#after-parameters-are-set-onparameterssetasync) is called or completes. Also, object disposal code shouldn't assume that objects created during initialization or other lifecycle methods exist. |
| 18 | + |
| 19 | +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. |
| 20 | + |
| 21 | +Developer code must ensure that <xref:System.IAsyncDisposable> implementations don't take a long time to complete. |
| 22 | + |
| 23 | +For more information, see the introductory remarks of <xref:blazor/components/sync-context>. |
| 24 | + |
| 25 | +## Disposal of JavaScript interop object references |
| 26 | + |
| 27 | +Examples throughout the [JavaScript (JS) interop articles](xref:blazor/js-interop/index) demonstrate typical object disposal patterns: |
| 28 | + |
| 29 | +* 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. |
| 30 | + |
| 31 | +* When calling .NET from JS, as described in <xref:blazor/js-interop/call-dotnet-from-javascript>, dispose of a created <xref:Microsoft.JSInterop.DotNetObjectReference> either from .NET or from JS to avoid leaking .NET memory. |
| 32 | + |
| 33 | +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. |
| 34 | + |
| 35 | +At a minimum, always dispose objects created on the .NET side to avoid leaking .NET managed memory. |
| 36 | + |
| 37 | +## DOM cleanup tasks during component disposal |
| 38 | + |
| 39 | +For more information, see <xref:blazor/js-interop/index#dom-cleanup-tasks-during-component-disposal>. |
| 40 | + |
| 41 | +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>. |
| 42 | + |
| 43 | +## Synchronous `IDisposable` |
| 44 | + |
| 45 | +For synchronous disposal tasks, use <xref:System.IDisposable.Dispose%2A?displayProperty=nameWithType>. |
| 46 | + |
| 47 | +The following component: |
| 48 | + |
| 49 | +* Implements <xref:System.IDisposable> with the [`@implements`](xref:mvc/views/razor#implements) Razor directive. |
| 50 | +* Disposes of `obj`, which is a type that implements <xref:System.IDisposable>. |
| 51 | +* A null check is performed because `obj` is created in a lifecycle method (not shown). |
| 52 | + |
| 53 | +```razor |
| 54 | +@implements IDisposable |
| 55 | +
|
| 56 | +... |
| 57 | +
|
| 58 | +@code { |
| 59 | + ... |
| 60 | +
|
| 61 | + public void Dispose() |
| 62 | + { |
| 63 | + obj?.Dispose(); |
| 64 | + } |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +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>. |
| 69 | + |
| 70 | +:::moniker range=">= aspnetcore-9.0" |
| 71 | + |
| 72 | +`TimerDisposal1.razor`: |
| 73 | + |
| 74 | +:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal1.razor"::: |
| 75 | + |
| 76 | +:::moniker-end |
| 77 | + |
| 78 | +:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" |
| 79 | + |
| 80 | +`TimerDisposal1.razor`: |
| 81 | + |
| 82 | +:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal1.razor"::: |
| 83 | + |
| 84 | +:::moniker-end |
| 85 | + |
| 86 | +:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" |
| 87 | + |
| 88 | +`CounterWithTimerDisposal1.razor`: |
| 89 | + |
| 90 | +:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor"::: |
| 91 | + |
| 92 | +:::moniker-end |
| 93 | + |
| 94 | +:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" |
| 95 | + |
| 96 | +`CounterWithTimerDisposal1.razor`: |
| 97 | + |
| 98 | +:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor"::: |
| 99 | + |
| 100 | +:::moniker-end |
| 101 | + |
| 102 | +:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" |
| 103 | + |
| 104 | +`CounterWithTimerDisposal1.razor`: |
| 105 | + |
| 106 | +:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor"::: |
| 107 | + |
| 108 | +:::moniker-end |
| 109 | + |
| 110 | +:::moniker range="< aspnetcore-5.0" |
| 111 | + |
| 112 | +`CounterWithTimerDisposal1.razor`: |
| 113 | + |
| 114 | +:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor"::: |
| 115 | + |
| 116 | +:::moniker-end |
| 117 | + |
| 118 | +> [!NOTE] |
| 119 | +> 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>. |
| 120 | +
|
| 121 | +If the object is created in a lifecycle method, such as [`OnInitialized{Async}`](xref:blazor/components/lifecycle#component-initialization-oninitializedasync), check for `null` before calling `Dispose`. |
| 122 | + |
| 123 | +:::moniker range=">= aspnetcore-9.0" |
| 124 | + |
| 125 | +`TimerDisposal2.razor`: |
| 126 | + |
| 127 | +:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal2.razor"::: |
| 128 | + |
| 129 | +:::moniker-end |
| 130 | + |
| 131 | +:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" |
| 132 | + |
| 133 | +`TimerDisposal2.razor`: |
| 134 | + |
| 135 | +:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal2.razor"::: |
| 136 | + |
| 137 | +:::moniker-end |
| 138 | + |
| 139 | +:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" |
| 140 | + |
| 141 | +`CounterWithTimerDisposal2.razor`: |
| 142 | + |
| 143 | +:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor"::: |
| 144 | + |
| 145 | +:::moniker-end |
| 146 | + |
| 147 | +:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" |
| 148 | + |
| 149 | +`CounterWithTimerDisposal2.razor`: |
| 150 | + |
| 151 | +:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor"::: |
| 152 | + |
| 153 | +:::moniker-end |
| 154 | + |
| 155 | +:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0" |
| 156 | + |
| 157 | +`CounterWithTimerDisposal2.razor`: |
| 158 | + |
| 159 | +:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor"::: |
| 160 | + |
| 161 | +:::moniker-end |
| 162 | + |
| 163 | +:::moniker range="< aspnetcore-5.0" |
| 164 | + |
| 165 | +`CounterWithTimerDisposal2.razor`: |
| 166 | + |
| 167 | +:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor"::: |
| 168 | + |
| 169 | +:::moniker-end |
| 170 | + |
| 171 | +For more information, see: |
| 172 | + |
| 173 | +* [Cleaning up unmanaged resources (.NET documentation)](/dotnet/standard/garbage-collection/unmanaged) |
| 174 | +* [Null-conditional operators ?. and ?[]](/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-) |
| 175 | + |
| 176 | +## Asynchronous `IAsyncDisposable` |
| 177 | + |
| 178 | +For asynchronous disposal tasks, use <xref:System.IAsyncDisposable.DisposeAsync%2A?displayProperty=nameWithType>. |
| 179 | + |
| 180 | +The following component: |
| 181 | + |
| 182 | +* Implements <xref:System.IAsyncDisposable> with the [`@implements`](xref:mvc/views/razor#implements) Razor directive. |
| 183 | +* Disposes of `obj`, which is an unmanaged type that implements <xref:System.IAsyncDisposable>. |
| 184 | +* A null check is performed because `obj` is created in a lifecycle method (not shown). |
| 185 | + |
| 186 | +```razor |
| 187 | +@implements IAsyncDisposable |
| 188 | +
|
| 189 | +... |
| 190 | +
|
| 191 | +@code { |
| 192 | + ... |
| 193 | +
|
| 194 | + public async ValueTask DisposeAsync() |
| 195 | + { |
| 196 | + if (obj is not null) |
| 197 | + { |
| 198 | + await obj.DisposeAsync(); |
| 199 | + } |
| 200 | + } |
| 201 | +} |
| 202 | +``` |
| 203 | + |
| 204 | +For more information, see: |
| 205 | + |
| 206 | +* <xref:blazor/components/sync-context> |
| 207 | +* [Cleaning up unmanaged resources (.NET documentation)](/dotnet/standard/garbage-collection/unmanaged) |
| 208 | +* [Null-conditional operators ?. and ?[]](/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-) |
| 209 | + |
| 210 | +## Assignment of `null` to disposed objects |
| 211 | + |
| 212 | +Usually, 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: |
| 213 | + |
| 214 | +* If the object's type is poorly implemented and doesn't tolerate repeat calls to <xref:System.IDisposable.Dispose%2A>/<xref:System.IAsyncDisposable.DisposeAsync%2A>, assign `null` after disposal to gracefully skip further calls to <xref:System.IDisposable.Dispose%2A>/<xref:System.IAsyncDisposable.DisposeAsync%2A>. |
| 215 | +* If a long-lived process continues to hold a reference to a disposed object, assigning `null` allows the [garbage collector](/dotnet/standard/garbage-collection/fundamentals) to free the object in spite of the long-lived process holding a reference to it. |
| 216 | + |
| 217 | +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`. |
| 218 | + |
| 219 | +## `StateHasChanged` |
| 220 | + |
| 221 | +> [!NOTE] |
| 222 | +> Calling <xref:Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged%2A> in `Dispose` and `DisposeAsync` isn'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. |
| 223 | +
|
| 224 | +## Event handlers |
| 225 | + |
| 226 | +Always unsubscribe event handlers from .NET events. The following [Blazor form](xref:blazor/forms/index) examples show how to unsubscribe an event handler in the `Dispose` method. |
| 227 | + |
| 228 | +Private field and lambda approach: |
| 229 | + |
| 230 | +```razor |
| 231 | +@implements IDisposable |
| 232 | +
|
| 233 | +<EditForm ... EditContext="editContext" ...> |
| 234 | + ... |
| 235 | + <button type="submit" disabled="@formInvalid">Submit</button> |
| 236 | +</EditForm> |
| 237 | +
|
| 238 | +@code { |
| 239 | + ... |
| 240 | +
|
| 241 | + private EventHandler<FieldChangedEventArgs>? fieldChanged; |
| 242 | +
|
| 243 | + protected override void OnInitialized() |
| 244 | + { |
| 245 | + editContext = new(model); |
| 246 | +
|
| 247 | + fieldChanged = (_, __) => |
| 248 | + { |
| 249 | + ... |
| 250 | + }; |
| 251 | +
|
| 252 | + editContext.OnFieldChanged += fieldChanged; |
| 253 | + } |
| 254 | +
|
| 255 | + public void Dispose() |
| 256 | + { |
| 257 | + editContext.OnFieldChanged -= fieldChanged; |
| 258 | + } |
| 259 | +} |
| 260 | +``` |
| 261 | + |
| 262 | +Private method approach: |
| 263 | + |
| 264 | +```razor |
| 265 | +@implements IDisposable |
| 266 | +
|
| 267 | +<EditForm ... EditContext="editContext" ...> |
| 268 | + ... |
| 269 | + <button type="submit" disabled="@formInvalid">Submit</button> |
| 270 | +</EditForm> |
| 271 | +
|
| 272 | +@code { |
| 273 | + ... |
| 274 | +
|
| 275 | + protected override void OnInitialized() |
| 276 | + { |
| 277 | + editContext = new(model); |
| 278 | + editContext.OnFieldChanged += HandleFieldChanged; |
| 279 | + } |
| 280 | +
|
| 281 | + private void HandleFieldChanged(object sender, FieldChangedEventArgs e) |
| 282 | + { |
| 283 | + ... |
| 284 | + } |
| 285 | +
|
| 286 | + public void Dispose() |
| 287 | + { |
| 288 | + editContext.OnFieldChanged -= HandleFieldChanged; |
| 289 | + } |
| 290 | +} |
| 291 | +``` |
| 292 | + |
| 293 | +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. |
| 294 | + |
| 295 | +## Anonymous functions, methods, and expressions |
| 296 | + |
| 297 | +When [anonymous functions](/dotnet/csharp/programming-guide/statements-expressions-operators/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. |
| 298 | + |
| 299 | +Anonymous lambda method approach (explicit disposal not required): |
| 300 | + |
| 301 | +```csharp |
| 302 | +private void HandleFieldChanged(object sender, FieldChangedEventArgs e) |
| 303 | +{ |
| 304 | + formInvalid = !editContext.Validate(); |
| 305 | + StateHasChanged(); |
| 306 | +} |
| 307 | + |
| 308 | +protected override void OnInitialized() |
| 309 | +{ |
| 310 | + editContext = new(starship); |
| 311 | + editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e); |
| 312 | +} |
| 313 | +``` |
| 314 | + |
| 315 | +Anonymous lambda expression approach (explicit disposal not required): |
| 316 | + |
| 317 | +```csharp |
| 318 | +private ValidationMessageStore? messageStore; |
| 319 | + |
| 320 | +[CascadingParameter] |
| 321 | +private EditContext? CurrentEditContext { get; set; } |
| 322 | + |
| 323 | +protected override void OnInitialized() |
| 324 | +{ |
| 325 | + ... |
| 326 | + |
| 327 | + messageStore = new(CurrentEditContext); |
| 328 | + |
| 329 | + CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear(); |
| 330 | + CurrentEditContext.OnFieldChanged += (s, e) => |
| 331 | + messageStore.Clear(e.FieldIdentifier); |
| 332 | +} |
| 333 | +``` |
| 334 | + |
| 335 | +The full example of the preceding code with anonymous lambda expressions appears in the <xref:blazor/forms/validation#validator-components> article. |
| 336 | + |
| 337 | +For more information, see [Cleaning up unmanaged resources](/dotnet/standard/garbage-collection/unmanaged) and the topics that follow it on implementing the `Dispose` and `DisposeAsync` methods. |
| 338 | + |
| 339 | +## Disposal during JS interop |
| 340 | + |
| 341 | +Trap <xref:Microsoft.JSInterop.JSDisconnectedException> in potential cases where loss of Blazor's SignalR circuit prevents JS interop calls and results an unhandled exception. |
| 342 | + |
| 343 | +For more information, see the following resources: |
| 344 | + |
| 345 | +* [JavaScript isolation in JavaScript modules](xref:blazor/js-interop/call-javascript-from-dotnet#javascript-isolation-in-javascript-modules) |
| 346 | +* [JavaScript interop calls without a circuit](xref:blazor/js-interop/index#javascript-interop-calls-without-a-circuit) |
0 commit comments