Post
Bringing Vue-style state back to Knockout
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:
const state = reactive({
name: 'Alice',
address: { city: 'The Hague' }
})
Nested properties are reactive. In Knockout, this is not the same thing:
const myObj = ko.observable({})
What you get:
myObjitself is observable- the object inside is a plain JavaScript object
myObj().foo = 123does not notify bindings
Only replacing the whole value notifies subscribers:
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
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
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:
ko.mapping.fromJS(newData, {}, viewModel)
Convert back to plain JS:
const plain = ko.mapping.toJS(viewModel)
Docs: Knockout Mapping plugin
Vue’s reactive() → Knockout helper
Treat mapping as your reactive():
function reactive(data) {
return ko.mapping.fromJS(data)
}
Usage then feels familiar — with Knockout’s getter/setter call style:
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.
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:
<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:
| Vue | Knockout (this pattern) |
|---|---|
reactive(data) | ko.mapping.fromJS(data) |
computed(() => …) | ko.computed(() => …) |
setup() returns { state, … } | same idea |
state.count | state.count() / state.count(n) in JS; state.count in bindings |
Why this works for me
- One
statebag — easy to scan, like Vue Composition API - Methods around
stateinstead 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.