Pick your starting point

Jump to the section that matches the version you're upgrading from.

Upgrading within v5.x (→ v5.3.0)

Most v5.x → v5.3.0 upgrades require no code changes. The new helpers/nav-tree and subtree: true features are purely additive, and the stacked use:active fix is backward compatible. The only breaking change in this version range is an older v5.2.0 removal (covered further down).

v5.3.0 — no migration needed

No breaking changes for stable consumers. The stacked use:active fix is backward compatible for the overwhelmingly common single-action-per-node case (existing tests pass unchanged). Adopt the new subtree: true option when you want to collapse two stacked use:active calls into one, and helpers/nav-tree when you want a single tree to drive both routes and a permission-filtered sidebar — see the What's New page for examples. disabled nav-tree nodes render as forbidden in both filter modes, and the new FilterOptions.disabledClassName lets you style "coming soon" placeholders distinctly from permission-denied items.

Pre-release adopters only: if you ran a 5.3.0-rc01 build and used the isHidden field on any nav-tree node, rename it to hidden — the field was renamed before stable release to match the KeenMate web-components convention (bare HTML-attribute names on data-model booleans, mirroring @keenmate/web-multiselect's MultiSelectOption.isDisableddisabled). The shape, semantics, and getter reactivity are unchanged — a pure find-and-replace. The helper predicate isNodeHidden(node) keeps its is* prefix because it's a function, not a field. If you're coming from a stable release (5.2.x or earlier), the field has always been hidden — nothing to do.

v5.2.1 — no migration needed

Single-issue release: bare-function routes ({ '/': Foo }) no longer throw Invalid component object under Svelte 5.5+ / Vite 7 / plugin-svelte 6. If you were on an older toolchain you wouldn't have hit it; if you were on the newer one you were probably workarounded with wrap({ component: Foo }) and can now drop that workaround. Either way: just upgrade.

⚠️ showToast removed from GlobalErrorHandler config (v5.2.0)

The built-in error toast is gone as of v5.2.0. The render guard was broken under the default navigateSafe strategy (the toast was effectively unobservable), so rather than patch it, the library now expects you to wire your own toast library inside the onError callback. TypeScript will flag the now-unknown property.

Toast handling moves to consumer
 

navigationContext() now returns null when nothing was passed (v5.2.0)

In v5.1.x and earlier, navigationContext() could return { _routeName: '/some-path' } after any push() — the router's internal _routeName key leaked through the public accessor, so any {#if !navigationContext()} branch was effectively unreachable. As of v5.2.0, the public accessor filters internal keys and returns null if no user-visible context exists.

If you were relying on the truthy-but-empty behavior to detect "navigation happened" (you almost certainly weren't), use location() for that. If you need the raw context including internal keys, import getRawNavigationContext() from /utils — but note that those keys are internal and may change.

routeContext() — use the current name

If you were importing routerouteContext() (a mangled name from an early find-replace accident) or the README's old routeUserData(), both are gone. The correct, current name is routeContext().

 

New features worth adopting

  • subtree: true on use:active (v5.3.0) — one action call for "parent stays active on its index AND on every nested URL"; pair with subtreeClassName for distinct styling
  • helpers/nav-tree (v5.3.0) — permission-aware filtering for tree-shaped menus; one tree drives both routes and sidebar
  • defineRoutes() (v5.2.0) — type-safe routes/nav/paths with full IDE autocomplete
  • setCurrentUser() (v5.2.0) — drop your custom getCurrentUser getter and get reactive hasPermission() for free
  • revalidateCurrentRoute() (v5.2.0) — re-check the active route on websocket permission updates without remounting
  • relativeLocation on event payloads (v5.2.0) — prefix-stripped path for nested routers

See What's New for full details on each.

Upgrading from v4.x to v5.0

Version 5.0 is a major release built for Svelte 5, featuring a complete rewrite using runes instead of stores. This section covers all breaking changes and provides step-by-step upgrade instructions.

Important: v5.0 requires Svelte 5.0 or later. If you're still on Svelte 4, continue using svelte-spa-router v4.x.

Breaking Changes Summary

Changev4.x (Old)v5.0 (New)
State managementSvelte storesSvelte 5 runes ($state, $derived)
State access$location, $paramslocation(), routeParams()
Parameter nameparamsrouteParams
Router eventsonrouteLoadingonRouteLoading (camelCase)
Logger APIsetDebugLoggingEnabled()enableLogging(), setCategoryLevel()
Unauthorized handlingHash-based navigation + onUnauthorizedComponent-based + configurePermissions()
Import paths/stores export path existsNo /stores path - removed

Step-by-Step Migration

1. Update Dependencies

 
 

2. Update State Access Patterns

Replace store subscriptions with function calls:

Old v4.x code
 
New v5.0 code
 

3. Update Route Component Props

Route components receive params via $props():

Old v4.x code
 
New v5.0 code
 

4. Update Router Event Handlers

Event handler props are now camelCase:

Old v4.x code
 
New v5.0 code
 

5. Update Logger API

The debug logging API has been completely redesigned:

Old v4.x code
 
New v5.0 code
 

6. Update Permission System

Unauthorized handling is now component-based:

Old v4.x code
 
New v5.0 code
 

7. Remove /stores Import Path

The /stores export path no longer exists:

 

Common Migration Issues

Issue: "Cannot read properties of undefined (reading 'before')"

Cause: Using sync component import with createProtectedRoute()

Solution: This is fixed in v5.0.0. Both patterns now work:

 

Issue: "enableCategory is not a function"

Cause: Using old logger API name

Solution: Use setCategoryLevel() instead:

 

Issue: "$params is not defined"

Cause: Trying to use store syntax with v5

Solution: Use function call with $derived:

 

Issue: "Missing './stores' specifier"

Cause: Trying to import from removed /stores path

Solution: Import from main module:

 

Migration Checklist

Before You Start
  • ☐ Back up your project
  • ☐ Review breaking changes list
  • ☐ Ensure tests exist for critical routes
Code Changes
  • ☐ Update Svelte to v5.0+
  • ☐ Update @keenmate/svelte-spa-router to v5.0
  • ☐ Replace all $location, $params, $querystring with function calls
  • ☐ Rename params to routeParams
  • ☐ Update Router event props to camelCase
  • ☐ Replace export let params with let { routeParams } = $props()
  • ☐ Update logger API calls
  • ☐ Update permission system configuration
  • ☐ Remove /stores import paths
Testing
  • ☐ Test all routes navigate correctly
  • ☐ Test route parameters work
  • ☐ Test protected routes and permissions
  • ☐ Test navigation guards and conditions
  • ☐ Test referrer tracking (if used)
  • ☐ Test error handling (if configured)
Cleanup
  • ☐ Remove old debug logging code
  • ☐ Update documentation
  • ☐ Review and remove unused imports

New Features to Explore

After migrating, consider adopting these new v5 features:

  • Category-based Logging: Fine-grained debug logging with 12 categories
  • Referrer Tracking: Automatic previous route tracking with scroll position restoration
  • goBack() Helper: Navigate to referrer with automatic scroll restoration
  • Hierarchical Routes: Automatic breadcrumb and permission inheritance
  • Global Error Handler: Comprehensive error handling with recovery strategies
  • Enhanced Permission System: Component-based unauthorized handling
  • Tree Route Structure: createHierarchy() for nested route definitions

See the What's New in v5 page for detailed information about all new features.

Getting Help

If you encounter issues during migration:

  • Check the API Reference for updated function signatures
  • Review example code in the documentation
  • Enable debug logging to troubleshoot routing issues
  • Open an issue on GitHub