Modern Development with Knockout

Something something old dog new tricks

Alex Bainter
7 min readMay 22, 2017

If you’ve so much as glanced at the internet in the last year, you’ve probably heard about React and Redux. React was confirmed as the sexiest framework of 2016, and Redux pairs with React like peanut butter and jelly.

I’ve had a great experience building apps with React and Redux. Unfortunately, using the latest hotness for projects isn’t always feasible. My team is already comfortable with Knockout.js and we have a great deal of Knockout apps we’ll need to support for the foreseeable future. Switching to React/Redux would mean training the entire team as well as new hires on React, Redux, and Knockout. To justify switching, a new framework would need to solve more headaches than it caused.

With that in mind, I set out to make building Knockout apps more like building React/Redux apps. Specifically, I wanted to:

1. Build apps out of independent, modular components (like React)
2. Separate app state from the view layer (like Redux)

After all, it’s not the tools themselves we love; a tool is just an implementation of a solution. We love the solutions behind the tools… right?

Components

One of my favorite parts of using React is separating my apps into small, reusable components. Knockout can painlessly replicate this with components and custom elements.

Just register components with Knockout:

function viewModel(params) {
const self = this;
self.count = ko.observable(params.initialCount);
self.increment = () => self.count(self.count() + 1);
}
const template =
`<div>
<span data-bind="text: count"></span>
<button type="button" data-bind="click: increment">
Increment
</button>
</div>`;
ko.components.register('counter', { viewModel, template });

Then, use them in markup:

<body>
<h1>Counter starting at 1:</h1>
<counter params="initialCount: 1"></counter>
</body>

Components can also be nested and support passing data from parent to child through the params object. The only issue is that we have to define our templates as strings. Yuck! If you’re already using some sort of build process on your files, this is easily resolved by loading .html files as strings. If you’re using webpack, this is exactly what html-loader is for. Then we can move our markup to a separate file:

<!-- counter.template.html -->
<div>
<span data-bind="text: count"></span>
<button type="button" data-bind="click: increment">
Increment
</button>
</div>

And change our registration to this:

import template from './counter.template.html';function viewModel(params) {
const self = this;
self.count = ko.observable(params.initialCount);
self.increment = () => self.count(self.count() + 1);
}
ko.components.register('counter', { viewModel, template });

Great, now we won’t get an ulcer from writing HTML strings. Since we’re playing with module bundlers, let’s add a loader for stylesheets as well. Then our components can import their own styles, too:

import template from './counter.template.html';
import './counter.styles.css';
function viewModel(params) {
const self = this;
self.count = ko.observable(params.initialCount);
self.increment = () => self.count(self.count() + 1);
}
ko.components.register('counter', { viewModel, template });

Now the behavior, markup, and styling for each component can live together, independent from the apps themselves. This makes our components modular and reusable, just like with React.

State Management

After building an app with Knockout components, I realized why React is most powerful with some sort of state management tool like Redux. Separating your app into smaller components makes them easier to maintain and reuse, but inevitably there will be some data which needs to be shared between multiple, different components. This data is the “state” of your app.

When you separate the app state (Redux) from the view layer (React), you can make changes to one without disturbing the other. Instead of manually ensuring all the pieces of your view are in sync with each other, we can just make sure each piece is in sync with the state, which is much easier to do.

I initially set out to use Redux with Knockout and create a knockout-redux package similar to react-redux. However, I quickly realized that Knockout and Redux weren’t quite right for each other.

Redux is all about unidirectional data flow. The view layer subscribes to the state’s store and re-renders when the state updates, but the view never modifies the state directly.

On the other hand, Knockout is more about two-way data binding with observables. Observables are already made to be subscribed to. This means our Knockout app’s state could just made up of observables, and Knockout will handle updating the state’s dependencies for us.

I built knockout-store as a replacement for using something like knockout-redux. Instead of shoehorning Redux into Knockout apps, knockout-store takes advantage of the tools already provided by Knockout (namely observables). It offers methods for getting and setting the state of your application and connecting components to the state.

First, we set the state of our entire application:

import { setState } from 'knockout-store';setState({ count: ko.observable(1) });

Then, we can connect our view models to the state with the connect method (which has been blatantly stolen from react-redux):

import { connect } from 'knockout-store';
import template from './counter.template.html';
import './counter.styles.css';
function viewModel(params) {
const self = this;
self.count = params.count;
self.increment = () => self.count(self.count() + 1);
}
function mapStateToParams({ count }) {
return { count };
}
const ConnectedViewModel = connect(mapStateToParams)(viewModel);ko.components.register('counter', {
template,
viewModel: ConnectedViewModel
});

If you’ve used react-redux, this probably looks eerily familiar. We define a mapStateToParams function instead of mapStateToProps. This function will be given the object passed to setState and any properties returned by it will be attached to the params object of our view model. mapStateToParams is passed to connect, which returns another function that will wrap the view model we call it with. The final result is our original view model with some extra properties from the state attached to the params object. You can read more about the motivation behind knockout-store in its wiki.

In our little example app, the result is that every counter component rendered on the page will display the same count. We can click any of the buttons to update the count for each component; it’s part of the app state. Since count is an observable, we can also subscribe to it or use it in computed functions, and Knockout will keep everything in sync for us. If we wanted to, we could even separate our counter component into presentational and container components just like a React/Redux app.

Differences from Redux and react-redux

While knockout-store was inspired by Redux and react-redux, the implementations are pretty different.

I’ve already mentioned knockout-store doesn’t adhere to unidirectional data flow. View models are free to modify the state directly; it’s really just an object they all have access to. Knockout will keep everything in sync as long as the state’s properties are observable, but it’s admittedly messier due to the nature of two-way data bindings.

When the state changes in a Redux app, the state object itself is replaced with a new one. In knockout-store, the state object is always the same object originally set with setState. Unfortunately, this means you can’t do the fun undo/redo history as easily as with a Redux app.

Finally, react-redux has a <Provider> component used to provide the state to the components using connect. I originally tried to replicate this, but I found using multiple stores for a Redux app isn’t recommended anyway. Because of this, I removed any concept of a <Provider> completely; this means you can only have one app state object per app, and connect automatically has access to it.

There are more differences but these are the big ones I can think of now.

What We Learned

After building an example todo app using components and knockout-store, I’m very pleased with the development experience. I enjoyed building the app for many of the same reasons I enjoy building React/Redux apps. I loved building it out of smaller, reusable components, and keeping the components decoupled from each other by separating the app state from the view layer. Would I still prefer to write React/Redux apps? Absolutely, but now I can still have a great time when it’s just not in the cards.

Turns out, it’s less about the tools we use and more about the problems they solve. In a few years, React is going to be old news and some new framework will be the new hotness (Vue?). Will we all need to jump ship right away?

If the cost of switching tools is high, spend some time trying to make your current tool behave the way you want. Perhaps you can borrow some ideas, or maybe you can find a compromise which maximizes benefits and minimizes costs. By all means, if it makes sense to switch, do it. Just be honest with yourself, and make sure you’re switching because it’s really the best way to go and not because you want to play with new toys (unless that’s a good enough reason in your situation).

I’ve also learned not to underestimate old tools. knockout-store is laughably small. Knockout provided a great sandbox for me to play in. Knockout is seven years old, which is like five hundred in software years. Yet, with a little thought, we can apply modern ideas to it and continue to make great apps. If my team does decide to start building React/Redux apps down the road, we’ll already have some of the concepts down.

We always describe new tools as “shiny.” Maybe our old ones just need a little polish.

Hi, I’m Alex. I make generative music at Generative.fm. Lots of developers (myself included) find it’s perfect for listening to while they code. It’s open source, too!

--

--

Alex Bainter
Alex Bainter

Written by Alex Bainter

A web developer creating audio/visual experiences both digital and not. Currently making generative music at Generative.fm.