blog

home / developersection / blogs / managing application state with routing in knockout.js spas

Managing Application State with Routing in Knockout.js SPAs

Managing Application State with Routing in Knockout.js SPAs

ICSM Computer 244 23-Apr-2025

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:

  1. Maps URL hashes or paths to views/components.
  2. Allows navigation without full page reloads.
  3. Supports back/forward browser navigation.
  4. Helps preserve/share user state via URL.

State:

  1. Is the data your application tracks (e.g. user info, selected item, form data).
  2. Changes based on interactions, and affects the UI.
  3. Together, routing and state allow SPAs to feel like full applications.

Managing Application State with Routing in Knockout.js SPAs

Implementing a Custom Router in Knockout

Let’s create a basic router that:

  1. Monitors window.location.hash
  2. Updates the currentRoute observable
  3. 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:

  1. Passing data between views.
  2. Updating views reactively as data changes.
  3. 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

  1. Centralize State: Keep shared state at the top level of your ViewModel.
  2. Use Computed Observables: For derived data or dynamic UI changes.
  3. Don’t Overcomplicate: Knockout is best for simpler SPAs. For more complex needs, look into integrating with libraries like Sammy.js for routing.
  4. 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.

 


Ravi Vishwakarma is a dedicated Software Developer with a passion for crafting efficient and innovative solutions. With a keen eye for detail and years of experience, he excels in developing robust software systems that meet client needs. His expertise spans across multiple programming languages and technologies, making him a valuable asset in any software development project.

Leave Comment

Comments

Liked By