Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 015 / 100

Lifecycle in Svelte 5 for Svelte & SvelteKit developers

onMount and effects.

Expona Admin7 min read

Understanding Component Lifecycle in Svelte 5

Managing when your component code runs is one of the most critical aspects of building reliable frontend applications. In Svelte 5, the approach to lifecycle has evolved significantly, giving us more predictable and efficient ways to handle initialization, cleanup, and reactive updates. Whether you're building a SvelteKit app that needs server-side rendering or a client-only component that manages WebSocket connections, understanding lifecycle in Svelte 5 for Svelte & SvelteKit developers will help you write cleaner, more maintainable code.

What is onMount and When Does It Run

The onMount lifecycle function is your entry point for code that should run after a component has been mounted to the DOM. This happens on the client side only, making it the ideal place to fetch data, initialize third-party libraries, or set up event listeners that require a real DOM element.

Think of a real-world scenario: you have a dashboard component that needs to load user analytics the moment it appears on screen. You don't want to fetch this data during server-side rendering—that would slow down the initial page load. Instead, you fetch it in onMount.

typescript
import { onMount } from 'svelte';

let analyticsData = $state([]);
let isLoading = $state(true);
let error = $state(null);

onMount(async () => {
  try {
    const response = await fetch('/api/analytics');
    if (!response.ok) throw new Error('Failed to load analytics');
    analyticsData = await response.json();
  } catch (err) {
    error = err.message;
  } finally {
    isLoading = false;
  }
});

In this example, the analytics data loads only after the component mounts on the client. The loading state and error handling provide feedback while the request is in flight. This is far better than blocking the server-side render or making the same request twice.

One key detail: onMount runs once per component instance, after all reactive statements have initialized. If you have a list of items and each item is its own component instance, each one gets its own onMount call.

Return Cleanup Functions from onMount

Components don't live forever. When a component is destroyed, any resources it allocated should be freed. That's where cleanup functions come in. Any function you return from onMount runs when the component is destroyed, giving you a chance to unsubscribe from event listeners, cancel pending requests, or close connections.

Consider a real scenario: you're building a live notifications panel that subscribes to a WebSocket. When the user closes the panel or navigates away, you need to unsubscribe to avoid memory leaks and unnecessary server load.

typescript
import { onMount } from 'svelte';

let notifications = $state([]);
let ws = null;

onMount(() => {
  ws = new WebSocket('wss://api.example.com/notifications');
  
  ws.onmessage = (event) => {
    const notification = JSON.parse(event.data);
    notifications = [...notifications, notification];
  };
  
  ws.onerror = () => {
    console.error('WebSocket error');
  };
  
  // Cleanup function: runs when component unmounts
  return () => {
    if (ws) {
      ws.close();
    }
  };
});

Without that cleanup function, the WebSocket would remain open even after the component is gone. In a SvelteKit app with client-side navigation, this would create multiple orphaned connections. The cleanup function ensures we always close the connection when the component is destroyed.

You can also use cleanup functions to cancel API requests, clear timers, or remove global event listeners. The pattern is consistent: any resource you acquire in onMount should be released in the cleanup function.

Effects: The Modern Replacement for Most Lifecycle Needs

Svelte 5 introduced $effect, a reactive statement that runs whenever its dependencies change. While onMount is still useful for one-time initialization, $effect handles most of the work that previously required a combination of onMount, onDestroy, and reactive statements.

The beauty of $effect is that it automatically tracks its dependencies. If you reference a reactive value (like a $state variable), the effect re-runs whenever that value changes. You don't have to manually specify a dependency array.

Let's say you're building a search component where results update as the user types. With effects, this becomes straightforward:

typescript
import { $effect } from 'svelte';

let searchQuery = $state('');
let results = $state([]);
let isSearching = $state(false);

$effect(async () => {
  if (!searchQuery.trim()) {
    results = [];
    return;
  }
  
  isSearching = true;
  try {
    const response = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`);
    results = await response.json();
  } catch (err) {
    console.error('Search failed:', err);
    results = [];
  } finally {
    isSearching = false;
  }
});

Every time searchQuery changes, this effect re-runs. The developer didn't need to manually list dependencies or worry about cleanup—Svelte handles it automatically. If a request is in flight and the query changes, the previous request's result gets ignored (the newer response overwrites it).

Effects can also return cleanup functions, just like onMount:

typescript
$effect(() => {
  const handleResize = () => {
    console.log('Window resized');
  };
  
  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize);
  };
});

This effect sets up a resize listener once and cleans it up if the component is destroyed. The cleanup function ensures no orphaned listeners are left behind.

Effects Do Not Run During Server-Side Rendering

This is a critical distinction that catches many developers off guard. When your SvelteKit component renders on the server, $effect does not run. This is by design. On the server, there's no DOM, no event listeners to attach, and no point in fetching data that the browser will immediately fetch again.

However, your reactive state initialization ($state) and reactive declarations ($derived) still run on the server. So the structure of your component is consistent, but the timing of effects is different.

Let's illustrate this with a theme switcher component in SvelteKit:

typescript
import { $effect } from 'svelte';

let theme = $state('light');

// This runs on both server and client
let isDarkMode = $derived(theme === 'dark');

// This only runs on the client after mounting
$effect(() => {
  // Read from localStorage (only exists on client)
  const savedTheme = localStorage.getItem('theme') || 'light';
  theme = savedTheme;
  
  // Apply theme to DOM
  document.documentElement.setAttribute('data-theme', theme);
});

$effect(() => {
  // Save theme changes to localStorage
  localStorage.setItem('theme', theme);
});

On the server, the component renders with the default theme of 'light'. When the HTML reaches the browser, the effects run. The first effect reads from localStorage and updates the theme, which triggers the second effect, which saves it back. This prevents localStorage errors during SSR and ensures the client-side experience is smooth.

If you need something to run on the server—like loading data before rendering—use SvelteKit's load functions in +page.server.ts or +layout.server.ts instead. Pass that data to your component as props, and use $effect to react to changes on the client.

Common Mistakes to Avoid

Forgetting cleanup functions in onMount: If you set up an event listener, subscription, or timer in onMount without returning a cleanup function, you'll create memory leaks. Always pair resource allocation with cleanup.

Using localStorage or DOM APIs during SSR: If your effect tries to access window or localStorage without checking if the component is running on the client, you'll get errors during server-side rendering. Use import { browser } from '$app/environment' to guard client-only code if needed, or rely on the fact that $effect doesn't run on the server.

Overcomplicating effects with manual dependency tracking: Developers coming from React sometimes try to manually list dependencies in Svelte effects. That's not how Svelte works. Just reference the reactive values you need, and Svelte tracks them automatically. Explicit tracking actually leads to bugs.

Mixing onMount and $effect for the same purpose: Choose one approach per use case. Use onMount for one-time initialization. Use $effect for reactive updates. Mixing them often leads to duplicate logic or unexpected timing issues.

Key Takeaways

  • onMount runs once after a component mounts on the client, making it ideal for data fetching and initialization that shouldn't happen on the server.
  • Return a cleanup function from onMount to close connections, remove listeners, or cancel requests when the component is destroyed.
  • $effect is the modern way to handle reactive updates in Svelte 5. It automatically tracks dependencies and re-runs when they change.
  • Effects can also return cleanup functions, ensuring resources are freed when the component unmounts or the effect is superseded by a new dependency change.
  • $effect does not run during server-side rendering. Use SvelteKit's load functions to fetch data on the server, and effects to handle client-side updates.
  • Avoid memory leaks by always cleaning up resources like WebSocket connections, event listeners, and timers.
  • Prefer $effect over onMount for most reactive scenarios in Svelte 5, reserving onMount for genuine one-time setup.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles