Skip to content

Observable ​

Create observable state ​

You can create an observable state in two ways: by extending the Observable class, or by applying the makeObservable function to an existing object.

ts
import { makeObservable } from 'kr-observable'

const state = makeObservable({ 
  foo: 'bar'
})
ts
import { Observable } from 'kr-observable'

class State extends Observable {
  foo = 'bar';
}

As a result, you get the following features out of the box:

  • All properties are made observable, including those added dynamically;
  • Values of type Array, Map, Set and plain objects become deeply observable;
  • All getters are treated as computed properties. Their values are cached and only re-evaluate when dependencies change;
  • All methods are automatically bound to target, so you don’t need to manually bind this;
js
class Foo extends Observable {
  method() {
    console.log(this);
  }
}

const { method } = new Foo();

setTimeout(method); // Foo πŸ‘Œ
queueMicrotask(method); // Foo πŸ‘Œ
addEventListener('click', method); // Foo πŸ‘Œ
js
const foo = makeObservable({
  method() {
    console.log(this);
  }
})

const { method } = foo;

setTimeout(method); // foo πŸ‘Œ
queueMicrotask(method); // foo πŸ‘Œ
addEventListener('click', method); // foo πŸ‘Œ

Additionally, with class syntax:

  • Private fields and methods are allowed. They remain private and are not made reactive;
js
class State extends Observable {
  #privateFiled = ''; // πŸ‘Œ

  #privateMethod() {} // πŸ‘Œ

  get #privateGetter() {} // πŸ‘Œ
  set #privateSetter(value) {} // πŸ‘Œ
}
  • Inheritance is allowed. You can safely extend reactive classes, and reactivity will work correctly across the inheritance chain.
js
class State extends Observable {
  a = 1;
}

class SecondState extends State {
  b = 2;
}

class ThirdState extends SecondState {
  c = 3;
}

console.log(new ThirdState()); // { a: 1, b: 2, c: 3 }

makeObservable requires plain objects

The makeObservable function is intended for plain objects (object literals {} or Object.create(null)), and not for instances of other built-in or custom classes, even if they are technically objects.

ts
import { makeObservable } from 'kr-observable'

makeObservable({}); // ok
makeObservable(Object.create(null)); // ok

class Foo {}
makeObservable(new Foo()); // throws error

makeObservable([]); // throws error

Observables stay native ​

An observable object remains a standard JavaScript object, with its properties left intact β€” neither redefined, converted into getter/setter pairs, nor transformed into constructs like Signals or Atoms.

Deep observability ​

By default, Observable makes plain Objects, Arrays, Maps, and Sets deeply observable. This means that nested changes like state.items.push(1) or state.cache.get('user')?.name = 'Alice' are automatically tracked and trigger reactions.

Syntax ​

ts
import { makeObservable } from 'kr-observable'

const state = makeObservable({ 
  map: new Map, // deeply observable
  set: new Set, // deeply observable
  array: [], // deeply observable
  object: {} // deeply observable
})
ts
import { Observable } from 'kr-observable'

class State extends Observable {
  map = new Map; // deeply observable
  set = new Set; // deeply observable
  array = []; // deeply observable
  object = {}; // deeply observable
}

Important notes for Map and Set

  1. Keys in Map are never wrapped or transformed
    Since Map keys can be any JavaScript value, including objects, functions, or symbols, they will be stored as-is, to preserve native identity semantics. Only the value is made deeply observable.
  2. Values in Set are also stored as-is
    Observable only tracks whether a value is present in the set, not mutations to the value itself.
    However, if you add an already observable object (e.g., one created with makeObservable), its internal mutations remain reactive, because the object itself is observable.

This design ensures full compatibility with native Map and Set behavior while keeping values reactive where it matters.

Custom and built-in classes aren't deeply observed

Instances of built-in or custom classes will not be automatically made deeply observable.

ts
import { Observable } from 'kr-observable';

class Foo {};
class Bar extends Observable {};

class Baz extends Observable {
  reg = new RegExp(); // non-deeply observable
  foo = new Foo(); // non-deeply observable
  bar = new Bar(); // deeply observable
}

Ignoring properties ​

You can prevent certain properties from being tracked by adding them to the ignore list. Once added, those properties won't become reactive, and changes to them won't cause any reactions or side effects.

The ignore list is simply a Set that holds the names of properties you want to exclude from observation.

Syntax ​

ts
import { makeObservable } from 'kr-observable'

const ignore = new Set(['id']);

// id will not be observable
makeObservable({ id: 1, name: '' }, ignore);
ts
import { Observable } from 'kr-observable';

class User extends Observable {
  static ignore = new Set(['id']);

  // id will not be observable
  id = 1;
  name = ''; 
}

Shallow observation ​

You can control which properties should not be made deeply observable by adding them to the shallow observation list. Once added, those properties will still be observed, but changes to their nested values won't be tracked recursively β€” meaning updates deep inside these properties won't trigger reactions or effects.

The shallow list is simply a Set that holds the names of properties you want to observe only at the top level.

Syntax ​

ts
import { makeObservable } from 'kr-observable'

const shallow = new Set(['arr']);

makeObservable(
  { 
    id: 1, 
    arr: [] // non-deeply observable
  }, 
  undefined, // instead of ignore list
  shallow
);
ts
import { Observable } from 'kr-observable';

class User extends Observable {
  static shallow = new Set(['arr']);

  id = 1;
  // non-deeply observable
  arr = []; 
}

Example ​

ts
import { Observable, autorun } from 'kr-observable';

class State extends Observable {
  static shallow = new Set(['a','b']);
  
  a = { foo: 1 };
  b = []; 
}

const state = new State()

state.a = {}; // will trigger reactions/effects
state.b = []; // will trigger reactions/effects
state.b.push(1); // won't trigger reactions/effects
state.a.foo += 1; // won't trigger reactions/effects
Inheritance of ignore and shallow properties

These properties are not automatically merged with those from parent classes.
For example, if you define a static ignore property in a subclass, it completely replaces the ignore list inherited from parent class:

ts
import { Observable } from 'kr-observable';

class User extends Observable {
  static ignore = new Set(['id']);
  
  id: string; // non-observable
  name: string; // observable
}

class SuperUser extends User {
   static ignore = new Set(['name']); // replaces User.ignore!
}

new SuperUser().id // observable! (likely not what you wanted)

To extend the parent’s ignored properties, explicitly include them:

ts
class SuperUser extends User {
   static ignore = new Set([...User.ignore, 'name']);
}

In most cases, there is no need to use ignore or shallow. These options become useful primarily when working with large or complex objects that you intentionally want to exclude from reactivity β€” for example, to optimize performance or prevent excessive tracking.

kroman@observable.ru