Summary
In the Svelte 5 (runes) variant of useAskable, focus and clear event listeners are registered synchronously during module initialization — not inside a lifecycle hook. There is no corresponding cleanup, so if the component unmounts, listeners remain on the context indefinitely.
Root cause
packages/svelte/src/useAskable.svelte.ts:
let focus: AskableFocus | null = $state(null);
ctx.on('focus', (f) => { focus = f; }); // registered at init, never removed
ctx.on('clear', () => { focus = null; }); // registered at init, never removed
if (options?.observe !== false) {
ctx.observe(document, observeOpts);
}
Unlike the store-based Svelte implementation which returns a destroy() function, the runes variant has no teardown path at all.
Expected behavior
Listeners should be registered in an effect with proper cleanup, or the function should return a destroy() handle the caller is responsible for calling.
Fix
Use $effect for registration and cleanup:
$effect(() => {
const handleFocus = (f: AskableFocus) => { focus = f; };
const handleClear = () => { focus = null; };
ctx.on('focus', handleFocus);
ctx.on('clear', handleClear);
return () => {
ctx.off('focus', handleFocus);
ctx.off('clear', handleClear);
};
});
Summary
In the Svelte 5 (runes) variant of
useAskable, focus and clear event listeners are registered synchronously during module initialization — not inside a lifecycle hook. There is no corresponding cleanup, so if the component unmounts, listeners remain on the context indefinitely.Root cause
packages/svelte/src/useAskable.svelte.ts:Unlike the store-based Svelte implementation which returns a
destroy()function, the runes variant has no teardown path at all.Expected behavior
Listeners should be registered in an effect with proper cleanup, or the function should return a
destroy()handle the caller is responsible for calling.Fix
Use
$effectfor registration and cleanup: