correctness/effect-as-onmount · Effect used as onMount
An $effect that reads no reactive value belongs in an event handler, {@attach}, or onMount instead.
Severity: warning · Category: correctness
What it checks
Flags an $effect / $effect.pre, in the component’s instance script, whose non-empty body reads no reactive value that this analysis can see: no $state, $derived, $props, imported binding, local declared with a new …() initializer, or store subscription, and no bare function call (foo()). Such an effect runs once after mount and never re-runs on the paths this check can follow.
Why it matters
An $effect that never reacts to anything is usually a slower, less direct stand-in for an event handler, {@attach}, or onMount. Using $effect for it obscures intent and misuses the reactivity system.
How to fix
<script>
import { onMount } from 'svelte';
// Instead of: $effect(() => { element.focus(); });
onMount(() => {
element.focus();
});
</script>
If the effect runs in response to a user interaction, prefer an event handler over $effect. If it syncs an element with an external library (e.g. a chart or a tooltip), prefer {@attach ...} (5.29+) instead. It’s the current recommended way to do DOM/library sync, ahead of both $effect and onMount.
Known limitations
This check recognizes a reactive read through a name it can trace back to a rune declarator ($state/$derived/$props), an imported binding, or a local declared with a new …() initializer (const x = new Foo()), which covers class $state fields, SvelteMap/SvelteSet, an imported runes-module state object, and svelte/reactivity/window. Two shapes have no such name to trace, so a genuinely reactive effect built either way can still be flagged:
- A reactive value reached only through a plain function’s return value (
const c = createCounter()). - A local assigned
new …()after its declaration instead of at it (let m; m = new SvelteMap();): only the declarator-init form is recognized.
Name matching is by identifier text, not lexical scope, at the same granularity this rule uses for rune names, so a callback-local binding that shadows an imported or new-declared name (e.g. a parameter reusing that name) is still treated as reactive. That can only suppress a finding, never wrongly flag one.
If that’s your case, suppress it with a
svelte-vitals-disable-next-line comment rather than moving working code into onMount.
Mode differences
None. This rule reads source, the same .svelte and .ts files, everywhere it runs. The CLI, the Vite plugin’s build pass, and the live dashboard’s static baseline all report it identically, and the rendered-HTML pass never re-evaluates it. Scoping a run with --route skips it: component-scoped rules have no route to attribute a finding to.
Disabling
Silence a single occurrence with <!-- svelte-vitals-disable-next-line correctness/effect-as-onmount --> on the line above it, or turn the rule off:
export default {
rules: {
'correctness/effect-as-onmount': 'off'
}
};