NexusCS

Svelte

Web Frameworks
Svelte is a radical new approach to building user interfaces. Instead of using techniques like virtual DOM diffing, Svelte writes code that surgically updates the DOM when the state of your app changes.
svelte
sveltekit
reactive
compiler
runes

Getting started

Introduction

Svelte is a compiler that generates minimal JavaScript code for reactive UIs. Svelte 5 introduces runes for explicit reactivity.

Installation (Svelte 5)

# Create new SvelteKit project
npm create svelte@latest my-app
cd my-app
npm install
npm run dev

Installation (Standalone Svelte)

# Vite + Svelte
npm create vite@latest my-app -- --template svelte
cd my-app
npm install
npm run dev

First Component

<script>
  let count = $state(0);

  function increment() {
    count++;
  }
</script>

<button onclick={increment}>
  Clicks: {count}
</button>

<style>
  button {
    font-size: 1.4rem;
  }
</style>

Svelte 5 Runes (Reactivity)

$state (Reactive State)

<script>
  // Basic state
  let count = $state(0);

  // Object state (deep reactivity)
  let user = $state({
    name: 'Alice',
    age: 30
  });

  // Array state
  let items = $state([1, 2, 3]);
</script>

<button onclick={() => count++}>
  {count}
</button>

<button onclick={() => user.age++}>
  {user.name} is {user.age}
</button>

Replaces let for reactive variables.

$derived (Computed Values)

<script>
  let count = $state(0);

  // Simple derived
  let doubled = $derived(count * 2);

  // Complex derived
  let message = $derived(() => {
    if (count > 10) return 'High';
    if (count > 5) return 'Medium';
    return 'Low';
  });
</script>

<p>{count} × 2 = {doubled}</p>
<p>Status: {message}</p>

Replaces $: reactive statements.

$effect (Side Effects)

<script>
  let count = $state(0);

  // Run when count changes
  $effect(() => {
    console.log(`Count is ${count}`);
    document.title = `Count: ${count}`;
  });

  // Effect with cleanup
  $effect(() => {
    const timer = setInterval(() => {
      console.log('tick');
    }, 1000);

    return () => clearInterval(timer);
  });
</script>

Replaces $: for side effects.

$props (Component Props)

<script>
  // Destructure props
  let { name, age = 18 } = $props();

  // With types (TypeScript)
  let {
    count = 0,
    onIncrement
  }: {
    count?: number;
    onIncrement?: () => void;
  } = $props();
</script>

<p>{name} is {age} years old</p>
<button onclick={onIncrement}>
  Count: {count}
</button>

Replaces export let for props.

$bindable (Two-Way Binding)

<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte';
  let value = $state('');
</script>

<Child bind:value />
<p>Parent: {value}</p>

<!-- Child.svelte -->
<script>
  let { value = $bindable('') } = $props();
</script>

<input bind:value />

Allows parent to bind to child props.

Component Basics

Component Structure

<script>
  // Logic
  let name = $state('world');

  function greet() {
    alert(`Hello ${name}!`);
  }
</script>

<!-- Markup -->
<h1>Hello {name}!</h1>
<button onclick={greet}>Greet</button>

<style>
  /* Scoped styles */
  h1 {
    color: purple;
  }
</style>

Three sections: script, markup, style.

Script Context

<script context="module">
  // Shared between all instances
  let counter = 0;

  export function sharedUtil() {
    return 'shared';
  }
</script>

<script>
  // Per-instance
  let id = counter++;
</script>

<p>Instance {id}</p>

TypeScript Support

<script lang="ts">
  interface User {
    name: string;
    age: number;
  }

  let user = $state<User>({
    name: 'Alice',
    age: 30
  });

  let { config }: {
    config: AppConfig
  } = $props();
</script>

Props & Bindings

Passing Props

<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte';
  let count = $state(0);
</script>

<Child
  name="Alice"
  {count}
  active={true}
/>

<!-- Child.svelte -->
<script>
  let {
    name,
    count,
    active
  } = $props();
</script>

Shorthand: {count} = count={count}.

Input Bindings

<script>
  let text = $state('');
  let checked = $state(false);
  let selected = $state('');
  let number = $state(0);
</script>

<input bind:value={text} />
<input type="checkbox" bind:checked />
<select bind:value={selected}>
  <option>A</option>
  <option>B</option>
</select>
<input type="number" bind:value={number} />

Component Bindings

<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte';
  let value = $state('');
</script>

<Child bind:value />
<p>Bound: {value}</p>

<!-- Child.svelte -->
<script>
  let { value = $bindable('') } = $props();
</script>

<input bind:value />

Element Bindings

<script>
  let element = $state();
  let scrollY = $state(0);
  let width = $state(0);

  $effect(() => {
    if (element) {
      console.log(element);
    }
  });
</script>

<div bind:this={element}>
  <h1>Element</h1>
</div>

<svelte:window bind:scrollY />
<div bind:clientWidth={width}>
  Width: {width}
</div>

Logic Blocks

{#if} Conditional

<script>
  let count = $state(0);
</script>

{#if count > 10}
  <p>Count is high</p>
{:else if count > 5}
  <p>Count is medium</p>
{:else}
  <p>Count is low</p>
{/if}

{#each} Loops

<script>
  let items = $state([
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Banana' },
    { id: 3, name: 'Cherry' }
  ]);
</script>

{#each items as item (item.id)}
  <p>{item.name}</p>
{/each}

<!-- With index -->
{#each items as item, i (item.id)}
  <p>{i}: {item.name}</p>
{/each}

<!-- With else -->
{#each items as item (item.id)}
  <p>{item.name}</p>
{:else}
  <p>No items</p>
{/each}

Always use key for dynamic lists.

{#await} Promises

<script>
  let promise = $state(fetchData());

  async function fetchData() {
    const res = await fetch('/api/data');
    return await res.json();
  }
</script>

{#await promise}
  <p>Loading...</p>
{:then data}
  <p>Data: {data.value}</p>
{:catch error}
  <p>Error: {error.message}</p>
{/await}

<!-- Short form -->
{#await promise then data}
  <p>{data.value}</p>
{/await}

{#key} Reset

<script>
  let value = $state(0);
</script>

{#key value}
  <Component />
{/key}

Destroys and recreates on key change.

Events

DOM Events

<script>
  let count = $state(0);

  function handleClick(event) {
    console.log(event);
    count++;
  }
</script>

<button onclick={handleClick}>
  Click me
</button>

<!-- Inline -->
<button onclick={() => count++}>
  {count}
</button>

<!-- With modifiers -->
<button onclick|preventDefault={handleClick}>
  Submit
</button>

Svelte 5 uses lowercase onclick, not on:click.

Event Modifiers

<button onclick|preventDefault={handle}>
  Prevent default
</button>

<button onclick|stopPropagation={handle}>
  Stop propagation
</button>

<button onclick|once={handle}>
  Fire once
</button>

<button onclick|capture={handle}>
  Capture phase
</button>

<!-- Chain modifiers -->
<button onclick|preventDefault|stopPropagation={handle}>
  Both
</button>

Custom Events (Svelte 4)

<!-- Child.svelte (Svelte 4) -->
<script>
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  function notify() {
    dispatch('message', {
      text: 'Hello'
    });
  }
</script>

<button on:click={notify}>Send</button>

<!-- Parent.svelte -->
<Child on:message={handleMessage} />

Callbacks (Svelte 5)

<!-- Child.svelte -->
<script>
  let { onMessage } = $props();

  function notify() {
    onMessage?.({ text: 'Hello' });
  }
</script>

<button onclick={notify}>Send</button>

<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte';

  function handleMessage(detail) {
    console.log(detail.text);
  }
</script>

<Child onMessage={handleMessage} />

Svelte 5 prefers callbacks over events.

Stores

Writable Store

// stores.ts
import { writable } from "svelte/store";

export const count = writable(0);
export const user = writable({
  name: "Alice",
  age: 30,
});
<!-- Component.svelte -->
<script>
  import { count } from './stores';

  // Auto-subscribe with $
  $count++;

  // Manual subscribe
  const unsubscribe = count.subscribe(value => {
    console.log(value);
  });
</script>

<p>Count: {$count}</p>
<button onclick={() => $count++}>
  Increment
</button>

$ prefix auto-subscribes/unsubscribes.

Readable Store

import { readable } from "svelte/store";

export const time = readable(new Date(), (set) => {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  return () => clearInterval(interval);
});
<script>
  import { time } from './stores';
</script>

<p>Time: {$time.toLocaleTimeString()}</p>

Derived Store

import { derived } from "svelte/store";
import { count } from "./stores";

export const doubled = derived(count, ($count) => $count * 2);

// Multiple stores
export const sum = derived([a, b], ([$a, $b]) => $a + $b);

// Async derived
export const users = derived(
  userIds,
  ($userIds, set) => {
    fetchUsers($userIds).then(set);
  },
  [], // Initial value
);

Custom Store

import { writable } from "svelte/store";

function createCounter() {
  const { subscribe, set, update } = writable(0);

  return {
    subscribe,
    increment: () => update((n) => n + 1),
    decrement: () => update((n) => n - 1),
    reset: () => set(0),
  };
}

export const counter = createCounter();
<script>
  import { counter } from './stores';
</script>

<p>{$counter}</p>
<button onclick={counter.increment}>+</button>
<button onclick={counter.decrement}>-</button>
<button onclick={counter.reset}>Reset</button>

Lifecycle

onMount

<script>
  import { onMount } from 'svelte';

  let data = $state();

  onMount(async () => {
    const res = await fetch('/api/data');
    data = await res.json();

    // Cleanup
    return () => {
      console.log('unmounting');
    };
  });
</script>

{#if data}
  <p>{data.value}</p>
{/if}

Runs after component mounted to DOM.

onDestroy

<script>
  import { onDestroy } from 'svelte';

  const interval = setInterval(() => {
    console.log('tick');
  }, 1000);

  onDestroy(() => {
    clearInterval(interval);
  });
</script>

beforeUpdate & afterUpdate

<script>
  import { beforeUpdate, afterUpdate } from 'svelte';

  beforeUpdate(() => {
    console.log('before DOM updates');
  });

  afterUpdate(() => {
    console.log('after DOM updates');
  });
</script>

tick

<script>
  import { tick } from 'svelte';

  let count = $state(0);

  async function increment() {
    count++;
    await tick(); // Wait for DOM update
    console.log('DOM updated');
  }
</script>

Awaits next DOM update.

SvelteKit Basics

File-Based Routing

src/routes/
├── +page.svelte          # /
├── about/
│   └── +page.svelte      # /about
├── blog/
│   ├── +page.svelte      # /blog
│   └── [slug]/
│       └── +page.svelte  # /blog/:slug
└── api/
    └── +server.ts        # /api endpoint

+page.svelte

<!-- src/routes/+page.svelte -->
<script>
  let { data } = $props();
</script>

<h1>{data.title}</h1>
<p>{data.content}</p>

+page.ts (Load Function)

// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from "./$types";

export const load: PageLoad = async ({ params, fetch }) => {
  const res = await fetch(`/api/posts/${params.slug}`);
  const post = await res.json();

  return {
    post,
  };
};

+page.server.ts (Server Load)

// src/routes/blog/+page.server.ts
import type { PageServerLoad } from "./$types";
import { db } from "$lib/db";

export const load: PageServerLoad = async () => {
  const posts = await db.posts.findMany();

  return {
    posts,
  };
};

Runs only on server, can access DB.

+layout.svelte

<!-- src/routes/+layout.svelte -->
<script>
  import Header from '$lib/Header.svelte';
  import Footer from '$lib/Footer.svelte';

  let { data, children } = $props();
</script>

<Header user={data.user} />

<main>
  {@render children()}
</main>

<Footer />

+layout.ts

// src/routes/+layout.ts
import type { LayoutLoad } from "./$types";

export const load: LayoutLoad = async ({ fetch }) => {
  const res = await fetch("/api/user");
  const user = await res.json();

  return {
    user,
  };
};

Data available to all child routes.

+server.ts (API Routes)

// src/routes/api/posts/+server.ts
import type { RequestHandler } from "./$types";
import { json } from "@sveltejs/kit";

export const GET: RequestHandler = async () => {
  const posts = await db.posts.findMany();
  return json(posts);
};

export const POST: RequestHandler = async ({ request }) => {
  const data = await request.json();
  const post = await db.posts.create({ data });
  return json(post, { status: 201 });
};

SvelteKit Navigation

Links

<script>
  import { page } from '$app/stores';
</script>

<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
  <a
    href="/blog"
    class:active={$page.url.pathname === '/blog'}
  >
    Blog
  </a>
</nav>

Programmatic Navigation

<script>
  import { goto } from '$app/navigation';

  function navigate() {
    goto('/about');
  }

  function goBack() {
    goto(-1); // History back
  }
</script>

<button onclick={navigate}>Go to About</button>
<button onclick={goBack}>Back</button>

Prefetching

<a href="/slow-page" data-sveltekit-preload-data>
  Preload on hover
</a>

<a href="/page" data-sveltekit-preload-data="tap">
  Preload on tap
</a>

<a href="/page" data-sveltekit-reload>
  Full page reload
</a>

Invalidation

<script>
  import { invalidate, invalidateAll } from '$app/navigation';

  async function refresh() {
    // Re-run load functions that depend on URL
    await invalidate('/api/posts');

    // Re-run all load functions
    await invalidateAll();
  }
</script>

Transitions & Animations

Built-in Transitions

<script>
  import { fade, fly, slide, scale } from 'svelte/transition';

  let visible = $state(true);
</script>

{#if visible}
  <div transition:fade>
    Fades in and out
  </div>

  <div transition:fly={{ y: 200, duration: 500 }}>
    Flies in from bottom
  </div>

  <div transition:slide>
    Slides in and out
  </div>

  <div transition:scale={{ start: 0.5 }}>
    Scales in and out
  </div>
{/if}

In/Out Transitions

<script>
  import { fade, fly } from 'svelte/transition';
</script>

{#if visible}
  <div
    in:fly={{ y: 200 }}
    out:fade
  >
    Different transitions
  </div>
{/if}

Custom Transitions

<script>
  function typewriter(node, { speed = 1 }) {
    const text = node.textContent;
    const duration = text.length / (speed * 0.01);

    return {
      duration,
      tick: t => {
        const i = Math.trunc(text.length * t);
        node.textContent = text.slice(0, i);
      }
    };
  }
</script>

<p transition:typewriter={{ speed: 1 }}>
  This text will type out
</p>

Animate Directive

<script>
  import { flip } from 'svelte/animate';
  import { fade } from 'svelte/transition';

  let items = $state([1, 2, 3, 4, 5]);

  function shuffle() {
    items = items.sort(() => Math.random() - 0.5);
  }
</script>

<button onclick={shuffle}>Shuffle</button>

{#each items as item (item)}
  <div animate:flip={{ duration: 300 }}>
    {item}
  </div>
{/each}

FLIP animation for list reordering.

Tweened

<script>
  import { tweened } from 'svelte/motion';
  import { cubicOut } from 'svelte/easing';

  const progress = tweened(0, {
    duration: 400,
    easing: cubicOut
  });
</script>

<progress value={$progress}></progress>

<button onclick={() => progress.set(0)}>0%</button>
<button onclick={() => progress.set(0.5)}>50%</button>
<button onclick={() => progress.set(1)}>100%</button>

Spring

<script>
  import { spring } from 'svelte/motion';

  const coords = spring({ x: 0, y: 0 }, {
    stiffness: 0.1,
    damping: 0.25
  });

  function handleMousemove(event) {
    coords.set({ x: event.clientX, y: event.clientY });
  }
</script>

<svelte:window onmousemove={handleMousemove} />

<div style="
  position: absolute;
  left: {$coords.x}px;
  top: {$coords.y}px;
">
  🎯
</div>

Actions

Basic Action

<script>
  function tooltip(node, text) {
    const tooltip = document.createElement('div');
    tooltip.textContent = text;

    function mouseenter() {
      document.body.appendChild(tooltip);
    }

    function mouseleave() {
      tooltip.remove();
    }

    node.addEventListener('mouseenter', mouseenter);
    node.addEventListener('mouseleave', mouseleave);

    return {
      destroy() {
        node.removeEventListener('mouseenter', mouseenter);
        node.removeEventListener('mouseleave', mouseleave);
      }
    };
  }
</script>

<button use:tooltip="Tooltip text">
  Hover me
</button>

Action with Parameters

<script>
  function longpress(node, duration = 500) {
    let timer;

    function handleMousedown() {
      timer = setTimeout(() => {
        node.dispatchEvent(new CustomEvent('longpress'));
      }, duration);
    }

    function handleMouseup() {
      clearTimeout(timer);
    }

    node.addEventListener('mousedown', handleMousedown);
    node.addEventListener('mouseup', handleMouseup);

    return {
      update(newDuration) {
        duration = newDuration;
      },
      destroy() {
        node.removeEventListener('mousedown', handleMousedown);
        node.removeEventListener('mouseup', handleMouseup);
      }
    };
  }
</script>

<button
  use:longpress={1000}
  onlongpress={() => console.log('long pressed')}
>
  Press and hold
</button>

Reusable Actions

// actions.ts
export function clickOutside(node: HTMLElement) {
  function handleClick(event: MouseEvent) {
    if (!node.contains(event.target as Node)) {
      node.dispatchEvent(new CustomEvent("outclick"));
    }
  }

  document.addEventListener("click", handleClick, true);

  return {
    destroy() {
      document.removeEventListener("click", handleClick, true);
    },
  };
}
<script>
  import { clickOutside } from './actions';

  let open = $state(false);
</script>

{#if open}
  <div
    use:clickOutside
    onoutclick={() => open = false}
  >
    Modal content
  </div>
{/if}

Slots & Snippets

Basic Slot (Svelte 4)

<!-- Card.svelte -->
<div class="card">
  <slot />
</div>

<!-- Usage -->
<Card>
  <h1>Title</h1>
  <p>Content</p>
</Card>

Named Slots (Svelte 4)

<!-- Card.svelte -->
<div class="card">
  <header>
    <slot name="header" />
  </header>
  <main>
    <slot />
  </main>
  <footer>
    <slot name="footer" />
  </footer>
</div>

<!-- Usage -->
<Card>
  <h1 slot="header">Title</h1>
  <p>Main content</p>
  <button slot="footer">Close</button>
</Card>

Snippets (Svelte 5)

<script>
  let { header, footer } = $props();
</script>

<div class="card">
  <header>
    {@render header()}
  </header>
  <main>
    {@render children()}
  </main>
  <footer>
    {@render footer()}
  </footer>
</div>
<!-- Usage -->
<Card>
  {#snippet header()}
    <h1>Title</h1>
  {/snippet}

  <p>Main content</p>

  {#snippet footer()}
    <button>Close</button>
  {/snippet}
</Card>

Svelte 5 replaces slots with snippets.

Snippet Props

<!-- List.svelte -->
<script>
  let { items, row } = $props();
</script>

<ul>
  {#each items as item (item.id)}
    <li>{@render row(item)}</li>
  {/each}
</ul>
<!-- Usage -->
<script>
  let items = $state([
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Banana' }
  ]);
</script>

<List {items}>
  {#snippet row(item)}
    <strong>{item.name}</strong>
  {/snippet}
</List>

Compiler Benefits

Why Svelte is Fast

Svelte compiles components to highly optimized vanilla JavaScript at build time, eliminating the need for a virtual DOM.

Key advantages:

  • No runtime overhead: No virtual DOM diffing
  • Smaller bundle size: Only what you use
  • Surgical updates: Direct DOM manipulation
  • True reactivity: Compile-time analysis

Bundle Size Comparison

Framework Hello World TodoMVC
Svelte ~2KB ~7KB
Vue ~16KB ~30KB
React ~40KB ~50KB

Performance Features

  • Automatic code splitting: Per-route in SvelteKit
  • CSS scoping: Unused CSS removed
  • Tree shaking: Dead code elimination
  • Ahead-of-time compilation: No runtime compilation

Developer Experience

  • Less boilerplate: No hooks, no HOCs
  • Intuitive reactivity: Just assign values
  • Built-in animations: No extra libraries
  • TypeScript support: First-class support

Common Patterns

Form Handling

<script>
  let formData = $state({
    name: '',
    email: '',
    message: ''
  });

  let errors = $state({});

  function validate() {
    errors = {};
    if (!formData.name) {
      errors.name = 'Name required';
    }
    if (!formData.email.includes('@')) {
      errors.email = 'Invalid email';
    }
    return Object.keys(errors).length === 0;
  }

  async function handleSubmit(event) {
    event.preventDefault();
    if (validate()) {
      const res = await fetch('/api/contact', {
        method: 'POST',
        body: JSON.stringify(formData)
      });
      // Handle response
    }
  }
</script>

<form onsubmit={handleSubmit}>
  <input
    bind:value={formData.name}
    placeholder="Name"
  />
  {#if errors.name}
    <p class="error">{errors.name}</p>
  {/if}

  <input
    type="email"
    bind:value={formData.email}
    placeholder="Email"
  />
  {#if errors.email}
    <p class="error">{errors.email}</p>
  {/if}

  <textarea
    bind:value={formData.message}
    placeholder="Message"
  />

  <button type="submit">Send</button>
</form>

Modal with Portal

<!-- Modal.svelte -->
<script>
  import { onMount } from 'svelte';
  import { fade } from 'svelte/transition';

  let { open = false, onClose } = $props();

  let portal;

  onMount(() => {
    portal = document.getElementById('portal');
  });
</script>

{#if open && portal}
  <div class="backdrop" transition:fade onclick={onClose}>
    <div
      class="modal"
      onclick|stopPropagation
    >
      {@render children()}
    </div>
  </div>
{/if}

Infinite Scroll

<script>
  import { onMount } from 'svelte';

  let items = $state([]);
  let page = $state(1);
  let loading = $state(false);
  let sentinel;

  async function loadMore() {
    if (loading) return;
    loading = true;

    const res = await fetch(`/api/items?page=${page}`);
    const newItems = await res.json();
    items = [...items, ...newItems];
    page++;
    loading = false;
  }

  onMount(() => {
    const observer = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting) {
        loadMore();
      }
    });

    observer.observe(sentinel);

    return () => observer.disconnect();
  });
</script>

<ul>
  {#each items as item (item.id)}
    <li>{item.name}</li>
  {/each}
</ul>

<div bind:this={sentinel}></div>

{#if loading}
  <p>Loading...</p>
{/if}

Context API

<!-- Parent.svelte -->
<script>
  import { setContext } from 'svelte';
  import Child from './Child.svelte';

  let theme = $state('dark');

  setContext('theme', {
    getTheme: () => theme,
    setTheme: (value) => theme = value
  });
</script>

<Child />

<!-- Child.svelte -->
<script>
  import { getContext } from 'svelte';

  const theme = getContext('theme');
  let currentTheme = $derived(theme.getTheme());
</script>

<button onclick={() => theme.setTheme('light')}>
  Current: {currentTheme}
</button>

Gotchas & Tips

Reactivity Pitfalls

<script>
  // ❌ Won't trigger reactivity (Svelte 4)
  let arr = [];
  arr.push(1); // Mutation doesn't trigger update

  // ✅ Reassign to trigger
  arr = [...arr, 1];

  // ✅ Or use $state (Svelte 5)
  let arr = $state([]);
  arr.push(1); // Works with $state!
</script>

Component Props

<script>
  // ❌ Don't mutate props directly
  let { count } = $props();
  count++; // Antipattern

  // ✅ Use callback or $bindable
  let { count = $bindable(0) } = $props();
  count++; // OK with $bindable
</script>

Async/Await in {#each}

<!-- ❌ Can't await in each -->
{#each promises as promise}
  {#await promise then data}
    <p>{data}</p>
  {/await}
{/each}

<!-- ✅ Await first, then iterate -->
{#await Promise.all(promises) then results}
  {#each results as data}
    <p>{data}</p>
  {/each}
{/await}

Key Blocks for Resets

<script>
  let id = $state(1);
</script>

<!-- Component persists when id changes -->
<Component {id} />

<!-- Component recreated when id changes -->
{#key id}
  <Component />
{/key}

Store Subscriptions

<script>
  import { count } from './stores';

  // ❌ Don't forget to unsubscribe
  count.subscribe(value => {
    console.log(value);
  }); // Memory leak!

  // ✅ Use $ or manually unsubscribe
  const unsubscribe = count.subscribe(value => {
    console.log(value);
  });

  onDestroy(unsubscribe);

  // ✅ Or just use $
  $: console.log($count);
</script>

Also see