My Blazor component has some associated JavaScript, which performs (async) animations.
MyComponent.razor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (someCondition & jsModule != null)
await jsModule.InvokeVoidAsync("startAnimation", "#my-component");
}
public async ValueTask DisposeAsync()
{
if (jsModule != null)
await jsModule.InvokeVoidAsync("stopAnimationPrematurely", "#my-component");
}
MyComponent.razor.js
export function startAnimation(id) {
let element = document.getElementById(id);
element.addEventListener(
'animationend',
function() { element.classList.remove("cool-animation") },
{ once: true }
);
element.classList.add("cool-animation");
}
export function stopAnimationPrematurely(id) {
let element = document.getElementById(id); // fails here
element.removeEventListener(
'animationend',
function() { element.classList.remove("cool-animation") },
{ once: true }
);
element.classList.remove("cool-animation");
}
As you can see, the animation cleans up after itself (via { once: true }).
However when the user clicks to a different page or component - and thus the blazor component is destroyed - there could be an animation in progress. If I don't remove the js event listener then I'll get a memory leak. So in DisposeAsync() I invoke js cleanup code which explicitly calls removeEventListener().
The problem is that by the time the js code runs, the component is already destroyed - so the DOM element is missing, the id is invalid, and thus the js cleanup fails (and throws).
This is very surprising. There is a race condition between the various lifecycle methods and disposal. In MudBlazor they also encountered this and introduced some really hard to understand (undocumented) locking as a workaround.
How can I deal with this without workarounds or hacks? (If that's not possible, please show a working solution, even using locking or whatever... a hacky solution is better than nothing.)