Skip to content

Svelte Integration ​

Problem ​

Svelte's $store auto-subscription expects store.subscribe(run) to invoke run immediately. router.subscribe() never synchronously emits the current snapshot, so it does not satisfy the Svelte store contract directly.

Solution ​

Wrap the router in readable(router.getSnapshot(), set => router.subscribe(set)) from svelte/store. This injects the current state as the initial value while router updates drive subsequent changes.

ts
// router.ts
import { createRouter } from '@vielzeug/wayfinder';
import { readable } from 'svelte/store';

export const router = createRouter({
  routes: {
    home: { path: '/' },
    settings: { path: '/settings' },
  },
  notFound: { data: () => ({ message: 'Not found' }) },
});

export const views = router.createViewRegistry(
  { home: HomePage, settings: SettingsPage },
  { notFound: NotFoundPage },
);

// Wraps the router in a Svelte-compatible readable store.
// `readable` calls set(initialValue) immediately, then router.subscribe drives updates.
export const routerState = readable(router.getSnapshot(), (set) => router.subscribe(set));

// Expose actions directly; router methods are safe to destructure.
export const { isActive, navigate, url } = router;
svelte
<!-- RouterView.svelte — exhaustive routes and an explicit fallback -->
<script lang="ts">
  import { routerState, views } from './router';

  $: component = views.resolve($routerState);
</script>

{#if component}
  <svelte:component this={component} />
{/if}
svelte
<!-- RouterLink.svelte -->
<script lang="ts">
  import { isActive, navigate, routerState, url } from './router';

  export let name: 'home' | 'settings';

  $: void $routerState;
  $: href = url(name);
  $: active = isActive(name);

  function go(event: MouseEvent) {
    event.preventDefault();
    void navigate({ name });
  }
</script>

<a aria-current={active ? 'page' : undefined} href={href} on:click={go}>
  <slot />
</a>

Using readable from svelte/store ensures the store gets its initial value synchronously and updates reactively on every navigation.

Pitfalls ​

  • Passing router directly as a Svelte store violates Svelte's synchronous initial-emission contract. Always use the readable wrapper.
  • The readable teardown is called when all subscribers unsubscribe. Do not call router.dispose() inside it unless you want to permanently shut down routing (e.g., end-to-end test cleanup).