Skip to content

Computed properties ​

A computed property is a getter that caches its value until one of the underlying observables has changed.

Syntax ​

ts
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.

ts
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:

text
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 :

  1. First, when autorun was initially called — this is expected behavior, as reactions run immediately on creation.
  2. Second, when the value of b changed from 2 to 1, causing isLess to become false.
  3. Third, when b changed from -3 to 5, making isLess return true 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:

ts
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:

text
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:

ts
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:

text
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.

ts
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:

text
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:

ts
if (state.isLess != null) {
  subscribe(state, () => {
    console.log('Run reaction')
  }, new Set(['isLess']))
}

And the output become:

text
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.

kroman@observable.ru