This is a critical part of SPA design, ensuring that navigation reflects the current state of your app and supports things like deep linking, browser history, and view transitions.
In any SPA, routing and state management go hand-in-hand. Routing determines what the user sees, while the application state determines how the view behaves. In Knockout.js, you can manage both efficiently using observables and a custom or third-party router.
Here’s how you can handle routing while keeping the application state reactive and in sync.
The Role of Routing in State Management
Routing:
- Maps URL hashes or paths to views/components.
- Allows navigation without full page reloads.
- Supports back/forward browser navigation.
- Helps preserve/share user state via URL.
State:
- Is the data your application tracks (e.g. user info, selected item, form data).
- Changes based on interactions, and affects the UI.
- Together, routing and state allow SPAs to feel like full applications.

Implementing a Custom Router in Knockout
Let’s create a basic router that:
- Monitors
window.location.hash - Updates the
currentRouteobservable - Syncs the application view with the URL
Step 1: Define Routes and States
function RouterViewModel() {
const self = this;
self.routes = {
home: {
template: 'home',
onEnter: function () {
self.message("Welcome to the home page!");
}
},
about: {
template: 'about',
onEnter: function () {
self.message("Here's some information about us.");
}
}
};
self.currentRoute = ko.observable('home');
self.message = ko.observable('');
self.currentTemplate = ko.computed(() => {
const route = self.routes[self.currentRoute()];
return route ? route.template : 'notfound';
});
self.routeTo = function (hash) {
const view = hash.replace('#', '');
if (self.routes[view]) {
self.currentRoute(view);
self.routes[view].onEnter?.();
} else {
self.currentRoute('notfound');
}
};
// Listen for hash changes
window.addEventListener('hashchange', () => self.routeTo(window.location.hash));
// Initial load
self.routeTo(window.location.hash || '#home');
}
Step 2: Bind the View
<nav>
<a href="#home">Home</a> |
<a href="#about">About</a>
</nav>
<div data-bind="template: { name: currentTemplate }"></div>
<p data-bind="text: message"></p>
<script type="text/html" id="home">
<h2>Home</h2>
<p>This is the homepage of our SPA.</p>
</script>
<script type="text/html" id="about">
<h2>About</h2>
<p>This page explains the SPA structure.</p>
</script>
<script type="text/html" id="notfound">
<h2>404</h2>
<p>Page not found.</p>
</script>
Step 3: Initialize the App
ko.applyBindings(new RouterViewModel());
Keeping State Between Routes
State variables like message, form fields, or user info should be stored as
shared observables in your root view model. This allows:
- Passing data between views.
- Updating views reactively as data changes.
- Persisting state if needed (e.g., in
localStorage).
Example:
self.userName = ko.observable("Guest");
self.routes.profile = {
template: 'profile',
onEnter: function () {
self.message("Editing profile for " + self.userName());
}
};
Advanced State Sync: Query Params & Deep Linking
Want to support query parameters or finer control like #profile?id=123?
Parse the hash using JavaScript:
function parseHash(hash) {
const [path, query] = hash.split('?');
const params = {};
if (query) {
query.split('&').forEach(part => {
const [key, value] = part.split('=');
params[key] = decodeURIComponent(value);
});
}
return { path: path.replace('#', ''), params };
}
Then use these parameters in your onEnter hook to load the appropriate data.
Best Practices
- Centralize State: Keep shared state at the top level of your ViewModel.
- Use Computed Observables: For derived data or dynamic UI changes.
- Don’t Overcomplicate: Knockout is best for simpler SPAs. For more complex needs, look into integrating with libraries like Sammy.js for routing.
- Avoid DOM Manipulation: Let Knockout handle the UI through bindings.
Bonus: Using Sammy.js for Routing
For better route handling, try Sammy.js:
<script src="https://cdnjs.cloudflare.com/ajax/libs/sammy.js/0.7.6/sammy.min.js"></script>
Sammy(function() {
this.get('#/home', function() {
viewModel.currentRoute('home');
});
this.get('#/about', function() {
viewModel.currentRoute('about');
});
}).run('#/home');
Conclusion
Managing application state alongside routing in Knockout.js doesn’t require heavy tooling. With observables and a simple hash-based routing mechanism, you can create responsive and reactive SPAs. For small to medium applications, this approach gives you control, flexibility, and minimal complexity—making Knockout.js a still-relevant player in the SPA world.
Leave Comment