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;
  • Getters are treated as computed properties, meaning their values are cached and only re-evaluated when dependencies change;
  • All methods are automatically bound to target, so you don’t need to manually bind this;

In addition, when using class syntax:

  • Private fields and methods (e.g., using #privateField syntax) are allowed β€” they remain private and are not made reactive;
    Example
    js
    class State extends Observable {
      // allowed
      #privateFiled = '';
      
      // allowed
      #privateMethod() {
      
      }
    
      // allowed
      get #privateGetter() {
      
      }
    }
  • Subclassing (inheritance) is allowed β€” you can safely extend reactive classes, and reactivity will work correctly across the inheritance chain.
    Example
    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 }

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.

Limitations ​

Custom and built-in classes aren't deeply observed

In addition to instances of Map, Set, Array, and plain Object, instances of other built-in or custom classes will not be automatically made deeply observable.

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

class Foo {};
class Bar extends Obsevable {};

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

makeObservable requires plain objects

The makeObservable function only accepts plain objects or objects without a prototype. Otherwise, it throws an error.

ts
import { makeObservable } from 'kr-observable'

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

class Foo {}
state = makeObservable(new Foo()); // error

state = makeObservable([]); // error

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 = ''; 
}

Limitations ​

Static properties are not inherited

If you prefer using class syntax, keep in mind that static properties such as ignore are not automatically inherited by subclasses. If you want to reuse or extend ignored properties from a parent class, you must explicitly redefine the ignore list in each subclass.

For example, in the code below, all properties of SuperUser will be observable, while in User, only name and role are observable β€” any ignored properties from the parent class won't take effect unless redeclared.

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

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

class SuperUser extends User { }

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 = []; 
}

When using class syntax

Similar to the ignore property, the shallow property is not automatically inherited. You must explicitly define it in each subclass if needed.

Example ​

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

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

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

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