Computed properties ​
A computed property is a getter that caches its value until one of the underlying observables has changed.
Syntax ​
import { Observable, makeObservable } from 'kr-observable'
class State extends Observable {
get prop() {
// ...
}
}
// or
makeObservable({
get prop() {
// ...
}
})
Ignore ​
All getters automatically become computed, but they can be ignored as well as properties.
How they work ​
The example below demonstrates how computed values work.
import { Observable, autorun } from 'kr-observable'
class State extends Observable {
a = 1
b = 2
get isLess() {
return this.a < this.b
}
}
const state = new State()
listen(state, (key, value) => {
console.log(`${key} changed`, 'Value:', value)
})
autorun(() => {
console.log('effect', state.isLess, '👌')
})
const timer = setInterval(() => {
state.b -= 1
}, 1000)
setTimeout(() => {
clearInterval(timer)
state.b = 5
}, 5010)
After running the code above, the console output will be:
isLess true 👌
b was changed. Value: 1
isLess false 👌
b was changed. Value: 0
b was changed. Value: -1
b was changed. Value: -2
b was changed. Value: -3
b was changed. Value: 5
isLess true 👌
As you can see, the reaction was triggered three times :
- First, when
autorun
was initially called — this is expected behavior, as reactions run immediately on creation. - Second, when the value of
b
changed from2
to1
, causingisLess
to becomefalse
. - Third, when
b
changed from-3
to5
, makingisLess
returntrue
again.
Meanwhile, b
was updated four more times, but those changes did not affect any tracked values, so they did not trigger unnecessary reactions — proving that the system efficiently tracks only what matters.
Tips ​
TIP
Computeds can derive values by combining data from multiple observables:
class Source1 extends Observable {
a = 1;
}
class Source2 extends Observable {
a = 1;
}
class State3 extends Observable {
one = new Source1();
two = new Source2();
get total() {
return this.one.a + this.two.a
}
}
const state = new State3()
autorun(() => {
console.log('total', state.total)
})
setTimeout(() => {
state.one.a += 1
}, 1000)
setTimeout(() => {
state.two.a += 1
}, 2000)
console output:
total 2
total 3
total 4
TIP
When using a getter/setter
pair, computed properties can derive values from external or non-observable state, enabling flexible reactivity patterns:
let value = 1
const state = makeObservable({
get value() {
return value
},
set value(newValue) {
value = newValue;
}
})
autorun(() => {
console.log(state.value)
})
setInterval(() => state.value = 2);
Output:
1
2
Demo on codepen.io
TIP
Observable includes a built-in structural comparator that is used by default when tracking dependencies in computed values, ensuring changes are detected based on actual value differences rather than reference equality.
Restrictions ​
Because computeds evaluate lazy, you can't subscribe to computed property until it was read. In example below, subscriber
won't be invoked.
class State extends Observable {
a = 1
b = 2
get isLess() {
return this.a < this.b
}
}
const state = new State()
listen(state, (key, value) => {
console.log(`${key} changed`, 'Value:', value)
})
subscribe(state, () => {
console.log('isLess was changed 👌')
}, new Set(['isLess']))
const timer = setInterval(() => {
state.b -= 1
}, 1000)
setTimeout(() => {
clearInterval(timer)
state.b = 5
}, 5010)
Output:
b was changed. Value: 1
b was changed. Value: 0
b was changed. Value: -1
b was changed. Value: -2
b was changed. Value: -3
b was changed. Value: 5
To resolve this, we can do the following:
if (state.isLess != null) {
subscribe(state, () => {
console.log('Run reaction')
}, new Set(['isLess']))
}
And the output become:
b was changed. Value: 1
isLess was changed 👌
b was changed. Value: 0
b was changed. Value: -1
b was changed. Value: -2
b was changed. Value: -3
b was changed. Value: 5
isLess was changed 👌
At their core, computed properties are enhanced getters — optimized for performance by memoizing results and only re-evaluating when relevant observables change.