Skip to content

Knockout.js Components — Reusable UI Widgets and Encapsulation

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Knockout.js Components. We cover key concepts, practical examples, and best practices to help you master this topic.

Knockout.js components are self-contained units of UI that combine a ViewModel and a template into a reusable custom element, enabling modular application architecture.

What You'll Learn

  • Registering components with ko.components.register()
  • Defining component ViewModel and template
  • Passing parameters to components
  • Component lifecycle and disposal
  • Loading components asynchronously

Why It Matters

As applications grow, repeating the same UI patterns (product cards, pagination controls, date pickers) in multiple places leads to duplication. Components encapsulate markup, logic, and styling in a single, reusable package.

Real-World Use

An e-commerce site with ProductCard, ReviewList, StarRating, AddToCartButton, and PriceDisplay components. Each component is developed and tested independently, then composed on category pages, search results, and product detail pages.

Component Architecture

flowchart TD
    A[ko.components.register] --> B[Component Definition]
    B --> C[ViewModel]
    B --> D[Template]
    C --> E[Observables & Computeds]
    C --> F[Lifecycle Hooks]
    D --> G[HTML Markup]
    D --> H[Bindings]
    E --> I[Custom Element]
    F --> I
    G --> I
    H --> I
    style B fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px

Registering a Basic Component

// Register a component with inline ViewModel and template
ko.components.register('star-rating', {
  viewModel: function(params) {
    var self = this;
    self.rating = ko.observable(params.rating || 0);
    self.maxRating = ko.observable(params.maxRating || 5);

    self.stars = ko.pureComputed(function() {
      var stars = [];
      for (var i = 1; i <= self.maxRating(); i++) {
        stars.push({
          filled: i <= self.rating(),
          index: i
        });
      }
      return stars;
    });

    self.setRating = function(star) {
      self.rating(star.index);
    };
  },
  template: '<div class="star-rating">\
    <span data-bind="foreach: stars">\
      <span data-bind="text: filled ? \'★\' : \'☆\',\
                       click: $parent.setRating,\
                       css: { filled: filled }"></span>\
    </span>\
    <span data-bind="text: rating"></span> / <span data-bind="text: maxRating"></span>\
  </div>'
});

Expected output: <star-rating params="rating: 3, maxRating: 5"></star-rating> renders 3 filled stars and 2 empty stars. Clicking a star updates the rating.

Using a Component in HTML

<!-- With static params -->
<star-rating params="rating: 4, maxRating: 5"></star-rating>

<!-- With observable params (two-way binding) -->
<star-rating params="rating: productRating, maxRating: 5"></star-rating>

<!-- Using component binding (alternative to custom element) -->
<div data-bind="component: { name: 'star-rating', params: { rating: 3, maxRating: 5 } }"></div>

Expected output: The component renders in place of the custom element tag. When productRating changes in the parent ViewModel, the component's rating parameter updates automatically.

Component with Separate Files

For larger components, use separate files:

// product-card.js
ko.components.register('product-card', {
  viewModel: { require: 'components/product-card' },
  template: { require: 'text!components/product-card.html' }
});

// components/product-card.js
define(['knockout'], function(ko) {
  return function(params) {
    this.product = params.product;
    this.addToCart = function() {
      params.cart.addItem(this.product);
    };
  };
});

// components/product-card.html
<div class="product-card">
  <h3 data-bind="text: product.name"></h3>
  <p data-bind="text: product.description"></p>
  <span class="price" data-bind="text: '$' + product.price.toFixed(2)"></span>
  <button data-bind="click: addToCart, enable: product.inStock">Add to Cart</button>
</div>

Component with AMD/RequireJS

ko.components.register('product-card', {
  viewModel: { require: 'components/product-card' },
  template: { require: 'text!components/product-card.html' }
});

For projects without a module loader, use string-based templates:

ko.components.register('simple-greeting', {
  viewModel: function(params) {
    this.name = ko.observable(params.name || 'World');
  },
  template: '<p>Hello, <span data-bind="text: name"></span>!</p>'
});

Passing Callbacks to Components

ko.components.register('confirm-button', {
  viewModel: function(params) {
    var self = this;
    self.label = params.label || 'Confirm';
    self.onConfirm = params.onConfirm; // Callback from parent

    self.confirm = function() {
      if (self.onConfirm && typeof self.onConfirm === 'function') {
        self.onConfirm();
      }
    };
  },
  template: '<button data-bind="click: confirm, text: label"></button>'
});

// Parent usage
// <confirm-button params="label: 'Delete', onConfirm: $root.deleteItem"></confirm-button>

Expected output: The component calls the parent's deleteItem function when its button is clicked, effectively communicating upward without tight coupling.

Component Lifecycle and Disposal

Components have a lifecycle that Knockout manages automatically:

ko.components.register('timer-display', {
  viewModel: function(params) {
    var self = this;
    self.elapsed = ko.observable(0);
    self.interval = setInterval(function() {
      self.elapsed(self.elapsed() + 1);
    }, 1000);

    // Cleanup: called when the component is removed from the DOM
    self.dispose = function() {
      clearInterval(self.interval);
      console.log('Timer disposed');
    };
  },
  template: '<span data-bind="text: elapsed"></span> seconds'
});

Expected output: The timer increments every second. When the component is removed (e.g., navigating away), dispose stops the interval, preventing memory leaks.

Async Component Loading

ko.components.register('heavy-component', {
  viewModel: {
    require: 'components/heavy-component',
    // Knockout shows a placeholder while loading
    createViewModel: function(params, componentInfo) {
      componentInfo.element.innerHTML = 'Loading...';
    }
  },
  template: { require: 'text!components/heavy-component.html' }
});

Component Communication Patterns

Components can communicate with parents through:

// 1. Observable parameters (reactive)
// Parent passes observable; component reads/writes it

// 2. Callback functions
// Parent passes a function; component calls it

// 3. Event aggregation
// Both subscribe to a shared event bus
var eventBus = new ko.subscribable();

ko.components.register('notification', {
  viewModel: function() {
    var self = this;
    self.messages = ko.observableArray([]);

    eventBus.subscribe(function(msg) {
      self.messages.push(msg);
    });
  },
  template: '<div data-bind="foreach: messages">\
    <div class="notification" data-bind="text: $data"></div>\
  </div>'
});

Common Mistakes

  1. Not defining a dispose function - Components that create timers, subscriptions, or event listeners must clean up in dispose(). Undisposed resources cause memory leaks.

  2. Overwriting params instead of reading them - params.rating is passed by reference for observables. Do not reassign params — read its properties.

  3. Using component binding with custom elements incorrectly - Custom elements must have a hyphen in their name (per Web Components spec). Use product-card, not productCard or productcard.

  4. Nesting components too deeply - Deep component trees are hard to debug. Limit nesting to 3-4 levels and use event aggregation for deep communication.

  5. Not handling loading states for async components - Async component loading takes time. Show a loading indicator or placeholder to avoid flickering.

Practice Questions

  1. What is the naming convention for Knockout component custom elements?
  2. How do you pass an observable from a parent to a child component?
  3. What method should you define in a component's ViewModel for cleanup?
  4. How can a child component communicate an action back to its parent?
  5. What are the two parts of a Knockout component definition?

Challenge: Build a pagination component that accepts totalItems, pageSize, and currentPage parameters. It should render page numbers, Previous/Next buttons, and call a callback when the page changes. Use the component on a product list page.

FAQ

Can Knockout components have their own CSS?

Not natively. Include component-specific CSS in the page or use a CSS-in-JS approach. The component template is just HTML with Knockout bindings.

How do components handle routing?

Components do not have built-in routing. Use a parent ViewModel with ko.observable to track the current view and conditionally render components with the component binding or if binding.

Can one component contain another component?

Yes, components can be nested arbitrarily. A ProductCard component might contain a StarRating component and an AddToCartButton component.

What happens if a component's required module fails to load?

Knockout logs an error and the component renders nothing. You can provide a fallback template using the afterRender or error handling patterns.

Are Knockout components the same as Web Components?

Knockout components use similar custom element syntax but are not native Web Components. They work only within the Knockout binding context.

Mini Project

Build a contact-card component that displays name, phone, email, and avatar. Create a contact-list component that uses contact-card internally. Add a search input to filter contacts. Each component should have proper disposal.

What's Next

Components use templates internally. Learn how to create and manage templates directly for more advanced rendering scenarios.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro