Web Loom logo
MVVM CoreMVVM in Svelte

MVVM in Svelte

How to wire Web Loom ViewModels into Svelte 5 components using reactive state and onMount/onDestroy lifecycle hooks. @web-loom/signals-core signals subscribe directly in Svelte lifecycle functions.

MVVM in Svelte

Status: No demo app exists yet. This page documents the integration pattern for teams adopting Svelte.

Svelte compiles components to vanilla JavaScript at build time — there is no runtime framework, no virtual DOM, and no component class. The compiler instruments reactive assignments directly in the generated output.

Web Loom ViewModels integrate cleanly with Svelte because the subscription mechanism is straightforward: subscribe to @web-loom/signals-core signals in onMount, assign values to Svelte reactive variables, unsubscribe in onDestroy.


Svelte's Reactivity Model

Svelte 5 uses runes — a set of compiler-understood primitives — for reactivity:

<script lang="ts">
  let count = $state(0);               // reactive variable
  let doubled = $derived(count * 2);   // computed value
</script>
 
<p>{count} × 2 = {doubled}</p>
<button onclick={() => count++}>Increment</button>

When count is assigned, Svelte surgically updates the DOM nodes that read it. Assignments trigger DOM updates; reads during rendering track dependencies.


Connecting a ViewModel

Subscribe to ViewModel signals with observe() in onMount and clean up in onDestroy:

<!-- GreenhouseList.svelte -->
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { greenHouseViewModel, type GreenhouseData } from '@repo/view-models/GreenHouseViewModel';
  import { observe } from '@web-loom/signals-core';
 
  let greenhouses = $state<GreenhouseData[]>([]);
  let isLoading   = $state(true);
 
  const teardowns: Array<() => void> = [];
 
  onMount(() => {
    teardowns.push(
      observe(greenHouseViewModel.data$, data => { greenhouses = data ?? []; }),
      observe(greenHouseViewModel.isLoading$, v => { isLoading = v; }),
    );
    greenHouseViewModel.fetchCommand.execute();
  });
 
  onDestroy(() => {
    teardowns.forEach(teardown => teardown());
    greenHouseViewModel.dispose();
  });
 
  function handleDelete(id: string) {
    greenHouseViewModel.deleteCommand.execute(id);
  }
</script>
 
{#if isLoading}
  <p>Loading…</p>
{:else}
  <ul>
    {#each greenhouses as gh (gh.id)}
      <li>
        {gh.name}
        <button onclick={() => handleDelete(gh.id!)}>Delete</button>
      </li>
    {/each}
  </ul>
{/if}

The ViewModel interaction — fetchCommand.execute(), deleteCommand.execute(id) — is identical to React, Vue, Angular, and Lit.


Svelte's Built-in Store Protocol (Alternative)

Svelte has a lightweight store protocol: any object implementing { subscribe(fn: (value: T) => void): () => void } can be auto-subscribed with the $ prefix. A Web Loom signal almost matches this protocol directly — sig.subscribe(fn) has the right shape and already returns a plain unsubscribe function — except the store protocol requires run to be called immediately with the current value on subscribe, and sig.subscribe() only fires on future changes. observe() supplies exactly that immediate-delivery behavior.

You can wrap a ViewModel signal in a Svelte-compatible store adapter:

// lib/toStore.ts
import { observe, type ReadonlySignal } from '@web-loom/signals-core';
import type { Readable } from 'svelte/store';
 
export function toStore<T>(sig: ReadonlySignal<T>): Readable<T> {
  return {
    subscribe(run) {
      return observe(sig, run);
    },
  };
}
<script lang="ts">
  import { toStore } from '$lib/toStore';
  import { greenHouseViewModel } from '@repo/view-models/GreenHouseViewModel';
 
  // Auto-subscribe with $ prefix — Svelte handles cleanup
  const greenhouses = toStore(greenHouseViewModel.data$);
</script>
 
<ul>
  {#each $greenhouses ?? [] as gh (gh.id)}
    <li>{gh.name}</li>
  {/each}
</ul>

The $greenhouses syntax auto-subscribes when the component mounts and unsubscribes when it destroys. No manual onMount/onDestroy needed.


Commands

Commands work as regular async function calls. For binding isExecuting$ to the UI, use the store adapter or subscribe manually:

<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { observe } from '@web-loom/signals-core';
 
  let isExecuting = $state(false);
  let unsubscribe: (() => void) | undefined;
 
  onMount(() => {
    unsubscribe = observe(greenHouseViewModel.fetchCommand.isExecuting$, v => { isExecuting = v; });
    greenHouseViewModel.fetchCommand.execute();
  });
  onDestroy(() => unsubscribe?.());
</script>
 
<button onclick={() => greenHouseViewModel.fetchCommand.execute()} disabled={isExecuting}>
  {isExecuting ? 'Loading…' : 'Refresh'}
</button>

Or with the toStore adapter:

<script lang="ts">
  const isExecuting = toStore(greenHouseViewModel.fetchCommand.isExecuting$);
</script>
 
<button disabled={$isExecuting}>
  {$isExecuting ? 'Loading…' : 'Refresh'}
</button>

SvelteKit Considerations

In SvelteKit, load functions run on the server. ViewModels that make HTTP calls should only run on the client — use onMount (which is client-only) rather than top-level module code.

For server-loaded initial data, fetch in +page.server.ts and pass as props. The ViewModel can then receive this data as the initial state, avoiding a client-side fetch on first load:

// +page.server.ts
export async function load() {
  const res = await fetch('http://api/greenhouses');
  return { initialGreenhouses: await res.json() };
}
<!-- +page.svelte -->
<script lang="ts">
  const { data } = $props();
  let greenhouses = $state(data.initialGreenhouses);
 
  onMount(() => {
    observe(greenHouseViewModel.data$, v => { greenhouses = v ?? greenhouses; });
  });
</script>

Summary

  • Reactive state$state() rune
  • Subscribe to ViewModelonMount(() => observe(vm.data$, ...))
  • UnsubscribeonDestroy(() => unsubscribe())
  • Auto-subscribe shorthandtoStore adapter + $storeName prefix
  • Render list{#each items as item}
  • Conditional render{#if condition}
  • Commandsvm.someCommand.execute(payload)
Was this helpful?
Web Loom logo
Copyright © Web Loom. All rights reserved.