onMount / onDestroy
onMount is the “run when the component is in the DOM” hook. Pair with cleanup for subscriptions; for async fetch, prefer top-level await + Suspense, or the SvelteKit load function.
onMount, onDestroy, async, alternatives
EXAMPLE
<script>
import { onMount, onDestroy, tick, beforeUpdate, afterUpdate, untrack } from 'svelte';
let inputEl;
let count = $state(0);
let online = $state(navigator.onLine);
let mounted = $state(false);
// 1) Focus an input on mount
onMount(() => {
inputEl.focus();
mounted = true;
});
// 2) Subscribe + clean up
onMount(() => {
const onChange = () => online = navigator.onLine;
window.addEventListener('online', onChange);
window.addEventListener('offline', onChange);
return () => {
window.removeEventListener('online', onChange);
window.removeEventListener('offline', onChange);
};
});
// 3) Interval / animation
onMount(() => {
const id = setInterval(() => count++, 1000);
return () => clearInterval(id);
});
// 4) Async work inside onMount (return for cleanup; use a flag for cancellation)
let data = $state(null);
onMount(() => {
let active = true;
(async () => {
const r = await fetch('/api/data');
const json = await r.json();
if (active) data = json;
})();
return () => { active = false; };
});
// 5) onDestroy — cleanup when the component is removed
onDestroy(() => {
// remove DOM listeners, cancel timers, dispose resources
console.log('component destroyed');
});
// 6) beforeUpdate / afterUpdate — DOM measurement timing
beforeUpdate(() => {
// about to re-render; read pre-update DOM state if needed
});
afterUpdate(() => {
// DOM is updated; safe to measure / scroll-to-bottom
});
// 7) tick() — wait until the next DOM flush
async function addAndScroll() {
messages.push(newMessage);
await tick(); // DOM has now updated
container.scrollTop = container.scrollHeight;
}
// 8) Compare to runes — $effect runs every time deps change
$effect(() => {
if (count > 5) {
console.log('count exceeded 5');
}
});
// $effect cleanup runs on dep change AND on unmount
// 9) Common bugs
// • Accessing the DOM in script top-level (before onMount) → element refs are null
// • Forgetting cleanup → leaks on remount (HMR, navigation)
// • Setting state inside onDestroy → unnecessary work; just clean resources
// • Heavy work in onMount → blocks first paint; defer with await tick() or setTimeout 0
</script>
<!-- 10) DOM ref pattern -->
<input bind:this={inputEl} placeholder="Search…" />
<!-- 11) Lifecycle order (Svelte 5) -->
<!--
script setup → $state, $derived initialised
onMount — after first DOM render
beforeUpdate / afterUpdate — around state changes
$effect — after relevant state changes
onDestroy — symmetric with onMount
-->
<!-- 12) SvelteKit alternative — load function (runs on server + client) -->
<!-- +page.js -->
<script context="module">
export async function load({ fetch, params }) {
const res = await fetch(`/api/posts/${params.id}`);
return { post: await res.json() };
}
</script>
<!-- Or +page.server.js for server-only -->
<!-- +page.server.js -->
<!--
import { db } from $lib/db';
export async function load({ params }) {
return { post: await db.posts.findById(params.id) };
}
-->
<!-- 13) Async setup with Suspense (experimental in Svelte 5) -->
<!-- {#await fetchData()} -->
<!-- <Spinner /> -->
<!-- {:then data} -->
<!-- <UserCard {data} /> -->
<!-- {:catch error} -->
<!-- <p>Error: {error.message}</p> -->
<!-- {/await} -->
<!-- 14) Real patterns -->
<!-- a) IntersectionObserver lazy-load images -->
<script>
import { onMount, onDestroy } from 'svelte';
let imgEl;
let loaded = $state(false);
let observer;
onMount(() => {
observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
loaded = true;
observer.disconnect();
}
});
observer.observe(imgEl);
});
onDestroy(() => observer?.disconnect());
</script>
<img
bind:this={imgEl}
src={loaded ? '/photo.jpg' : '/placeholder.svg'}
loading="lazy"
/>
<!-- b) Drag handler with cleanup -->
<script>
onMount(() => {
const onDown = (e) => start(e);
const onMove = (e) => move(e);
const onUp = (e) => end(e);
document.addEventListener('mousedown', onDown);
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
return () => {
document.removeEventListener('mousedown', onDown);
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
});
</script>
<!-- c) Initial fetch on mount -->
<script>
let users = $state([]);
let loading = $state(true);
let error = $state(null);
onMount(async () => {
try {
const r = await fetch('/api/users');
users = await r.json();
} catch (e) {
error = e.message;
} finally {
loading = false;
}
});
</script>
{#if loading}
<Spinner />
{:else if error}
<p class="error">{error}</p>
{:else}
<ul>
{#each users as user (user.id)}
<li>{user.name}</li>
{/each}
</ul>
{/if}
<!-- 15) Best practices -->
<!-- - For SSR-friendly data fetching, prefer SvelteKit load() over onMount -->
<!-- - Return a cleanup function from onMount when you subscribe / time / observe -->
<!-- - Read DOM in onMount, not in script setup -->
<!-- - For reactive side effects driven by state, use $effect instead of onMount/onDestroy -->
<!-- - Don't fetch data in onMount that the server could have rendered -->
Why it matters
For data, prefer SvelteKit’s load() over onMount — you get SSR, prefetch, and progressive enhancement for free. onMount is for DOM-touching work; \$effect handles reactive side effects more cleanly.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { onMount, onDestroy } from 'svelte';
onMount(() => console.log('mounted'));
onDestroy(() => console.log('bye'));
Try it Yourself »
Discussion
Loading…