Post

Bringing Vue-style state back to Knockout

Harianto van Insulinde

I still bump into Knockout now and then. And because I love Vue, I keep wanting the same shape of code: a grouped state, a setup() that returns state plus methods, and nested data that just updates the UI.

Knockout can get surprisingly close — once you stop treating ko.observable({}) like Vue’s reactive().

The trap: ko.observable({})

In Vue 3 you write:

js
const state = reactive({
  name: 'Alice',
  address: { city: 'The Hague' }
})

Nested properties are reactive. In Knockout, this is not the same thing:

js
const myObj = ko.observable({})

What you get:

  • myObj itself is observable
  • the object inside is a plain JavaScript object
  • myObj().foo = 123 does not notify bindings

Only replacing the whole value notifies subscribers:

js
myObj({ foo: 123, bar: 456 }) // this notifies

So ko.observable is for a single value. It tracks replacement, not deep property mutation.

Making properties observable

Manual view model

js
function Person(data) {
  this.id = ko.observable(data.id)
  this.name = ko.observable(data.name)
  this.age = ko.observable(data.age)
}

const person = new Person({ id: 1, name: 'Alice', age: 30 })

person.name()       // get
person.name('Bob')  // set

Fine for small shapes. Painful once nesting and arrays grow.

Mapping plugin (ko.mapping.fromJS)

This is the one I keep coming back to. The official mapping plugin turns plain data into a view model:

  • scalars → ko.observable
  • arrays → ko.observableArray
  • nested objects → objects of observables
js
const data = {
  name: 'Alice',
  age: 30,
  address: {
    city: 'The Hague',
    zip: '2511'
  },
  phones: [
    { type: 'mobile', number: '06-12345678' },
    { type: 'work', number: '070-123456' }
  ]
}

const viewModel = ko.mapping.fromJS(data)

viewModel.name()                 // 'Alice'
viewModel.age(31)                // update
viewModel.address.city()         // 'The Hague'
viewModel.phones()[0].number()   // '06-12345678'

Update in place:

js
ko.mapping.fromJS(newData, {}, viewModel)

Convert back to plain JS:

js
const plain = ko.mapping.toJS(viewModel)

Docs: Knockout Mapping plugin

Vue’s reactive() → Knockout helper

Treat mapping as your reactive():

js
function reactive(data) {
  return ko.mapping.fromJS(data)
}

Usage then feels familiar — with Knockout’s getter/setter call style:

js
const state = reactive({
  name: 'Alice',
  address: {
    city: 'The Hague',
    zip: '2511'
  }
})

state.name()                 // get
state.name('Bob')            // set
state.address.city()         // nested get
state.address.city('Delft')  // nested set

Important difference from the naive ko.observable({}) pattern: state here is not an observable wrapping an object. It is an object whose properties are observables. In bindings you write state.name, not state().name.

Grouped state + setup()

I like keeping all reactive data under one name: state. Logic lives around it. Very Composition-API-ish.

js
function reactive(data) {
  return ko.mapping.fromJS(data)
}

function setup() {
  const state = reactive({
    count: 0,
    user: {
      name: '',
      age: 0
    },
    items: []
  })

  const increment = () => {
    state.count(state.count() + 1)
  }

  const setUserName = (name) => {
    state.user.name(name)
  }

  const addItem = (text) => {
    state.items.push(reactive({ text, done: false }))
  }

  const doubleCount = ko.computed(() => state.count() * 2)

  return {
    state,
    increment,
    setUserName,
    addItem,
    doubleCount
  }
}

const vm = setup()
ko.applyBindings(vm)

Template:

html
<div>
  <p>Count: <span data-bind="text: state.count"></span></p>
  <p>Double: <span data-bind="text: doubleCount"></span></p>
  <button data-bind="click: increment">+</button>

  <p>User: <input data-bind="value: state.user.name" /></p>
  <button data-bind="click: () => setUserName('Alice')">Set Alice</button>

  <ul data-bind="foreach: state.items">
    <li data-bind="text: text"></li>
  </ul>
  <button data-bind="click: () => addItem('New item')">Add item</button>
</div>

Mental map:

VueKnockout (this pattern)
reactive(data)ko.mapping.fromJS(data)
computed(() => …)ko.computed(() => …)
setup() returns { state, … }same idea
state.countstate.count() / state.count(n) in JS; state.count in bindings

Why this works for me

  • One state bag — easy to scan, like Vue Composition API
  • Methods around state instead of a giant constructor prototype
  • Nested objects and arrays stay reactive without hand-wiring every property
  • You can still drop into classic Knockout when a binding or plugin expects it

Knockout will never be Vue. The parentheses in JS stay. But if you are maintaining KO and thinking in Vue, reactive + setup + grouped state is a structure worth bringing back.