Creating observable state · MobX 🇺🇦 (2024)

Properties, entire objects, arrays, Maps and Sets can all be made observable.The basics of making objects observable is specifying an annotation per property using makeObservable.The most important annotations are:

  • observable defines a trackable field that stores the state.
  • action marks a method as an action that will modify the state.
  • computed marks a getter that will derive new facts from the state and cache its output.

makeObservable

Usage:

  • makeObservable(target, annotations?, options?)

This function can be used to make existing object properties observable. Any JavaScript object (including class instances) can be passed into target.Typically makeObservable is used in the constructor of a class, and its first argument is this.The annotations argument maps annotations to each member. Only annotated members are affected.

Alternatively, decorators like @observable can be used on class members instead of calling makeObservable in the constructor.

Methods that derive information and take arguments (for example findUsersOlderThan(age: number): User[]) can not be annotated as computed – their read operations will still be tracked when they are called from a reaction, but their output won't be memoized to avoid memory leaks. To memoize such methods you can use MobX-utils computedFn {🚀} instead.

Subclassing is supported with some limitations by using the override annotation (see the example here).

class + makeObservable

class + decorators

factory function + makeAutoObservable

observable

class + decorators (legacy)

import { makeObservable, observable, computed, action, flow } from "mobx"

class Doubler {
value

constructor(value) {
makeObservable(this, {
value: observable,
double: computed,
increment: action,
fetch: flow
})
this.value = value
}

get double() {
return this.value * 2
}

increment() {
this.value++
}

*fetch() {
const response = yield fetch("/api/value")
this.value = response.json()
}
}

All annotated fields are non-configurable.
All non-observable (stateless) fields (action, flow) are non-writable.

When using modern decorators, there is no need to call makeObservable, below is what a decorator based class looks like.Note that the @observable annotation should always be used in combination with the accessor keyword.

import { observable, computed, action, flow } from "mobx"

class Doubler {
@observable accessor value

constructor(value) {
this.value = value
}

@computed
get double() {
return this.value * 2
}

@action
increment() {
this.value++
}

@flow
*fetch() {
const response = yield fetch("/api/value")
this.value = response.json()
}
}

import { makeAutoObservable } from "mobx"

function createDoubler(value) {
return makeAutoObservable({
value,
get double() {
return this.value * 2
},
increment() {
this.value++
}
})
}

Note that classes can leverage makeAutoObservable as well.The difference in the examples just demonstrate how MobX can be applied to different programming styles.

import { observable } from "mobx"

const todosById = observable({
"TODO-123": {
title: "find a decent task management system",
done: false
}
})

todosById["TODO-456"] = {
title: "close all tickets older than two weeks",
done: true
}

const tags = observable(["high prio", "medium prio", "low prio"])
tags.push("prio: for fun")

In contrast to the first example with makeObservable, observable supports adding (and removing) fields to an object.This makes observable great for collections like dynamically keyed objects, arrays, Maps and Sets.

To use legacy decorators, makeObservable(this) should be called in the constructor to make sure decorators work.

import { observable, computed, action, flow } from "mobx"

class Doubler {
@observable value

constructor(value) {
makeObservable(this)
this.value = value
}

@computed
get double() {
return this.value * 2
}

@action
increment() {
this.value++
}

@flow
*fetch() {
const response = yield fetch("/api/value")
this.value = response.json()
}
}

makeAutoObservable

Usage:

  • makeAutoObservable(target, overrides?, options?)

makeAutoObservable is like makeObservable on steroids, as it infers all the properties by default. You can however use the overrides parameter to override the default behavior with specific annotations —in particular false can be used to exclude a property or method from being processed entirely.Check out the code above for an example.

The makeAutoObservable function can be more compact and easier to maintain than using makeObservable, since new members don't have to be mentioned explicitly.However, makeAutoObservable cannot be used on classes that have super or are subclassed.

Inference rules:

  • All own properties become observable.
  • All getters become computed.
  • All setters become action.
  • All functions become autoAction.
  • All generator functions become flow. (Note that generator functions are not detectable in some transpiler configurations, if flow doesn't work as expected, make sure to specify flow explicitly.)
  • Members marked with false in the overrides argument will not be annotated. For example, using it for read only fields such as identifiers.

observable

Usage:

  • observable(source, overrides?, options?)
  • @observable accessor (field decorator)

The observable annotation can also be called as a function to make an entire object observable at once.The source object will be cloned and all members will be made observable, similar to how it would be done by makeAutoObservable.Likewise, an overrides map can be provided to specify the annotations of specific members.Check out the above code block for an example.

The object returned by observable will be a Proxy, which means that properties that are added later to the object will be picked up and made observable as well (except when proxy usage is disabled).

The observable method can also be called with collections types like arrays, Maps and Sets. Those will be cloned as well and converted into their observable counterparts.

Example: observable array

The following example creates an observable and observes it using autorun.Working with Map and Set collections works similarly.

import { observable, autorun } from "mobx"const todos = observable([ { title: "Spoil tea", completed: true }, { title: "Make coffee", completed: false }])autorun(() => { console.log( "Remaining:", todos .filter(todo => !todo.completed) .map(todo => todo.title) .join(", ") )})// Prints: 'Remaining: Make coffee'todos[0].completed = false// Prints: 'Remaining: Spoil tea, Make coffee'todos[2] = { title: "Take a nap", completed: false }// Prints: 'Remaining: Spoil tea, Make coffee, Take a nap'todos.shift()// Prints: 'Remaining: Make coffee, Take a nap'

Observable arrays have some additional nifty utility functions:

  • clear() removes all current entries from the array.
  • replace(newItems) replaces all existing entries in the array with new ones.
  • remove(value) removes a single item by value from the array. Returns true if the item was found and removed.

Note: primitives and class instances are never converted to observables

Primitive values cannot be made observable by MobX since they are immutable in JavaScript (but they can be boxed).Although there is typically no use for this mechanism outside libraries.

Class instances will never be made observable automatically by passing them to observable or assigning them to an observable property.Making class members observable is considered the responsibility of the class constructor.

{🚀} Tip: observable (proxied) versus makeObservable (unproxied)

The primary difference between make(Auto)Observable and observable is that the first one modifies the object you are passing in as first argument, while observable creates a clone that is made observable.

The second difference is that observable creates a Proxy object, to be able to trap future property additions in case you use the object as a dynamic lookup map.If the object you want to make observable has a regular structure where all members are known up-front, we recommend to use makeObservable as non proxied objects are a little faster, and they are easier to inspect in the debugger and console.log.

Because of that, make(Auto)Observable is the recommended API to use in factory functions.Note that it is possible to pass { proxy: false } as an option to observable to get a non proxied clone.

Available annotations

AnnotationDescription
observable
observable.deep
Defines a trackable field that stores state. If possible, any value assigned to observable is automatically converted to (deep) observable, autoAction or flow based on it's type. Only plain object, array, Map, Set, function, generator function are convertible. Class instances and others are untouched.
observable.refLike observable, but only reassignments will be tracked. The assigned values are completely ignored and will NOT be automatically converted to observable/autoAction/flow. For example, use this if you intend to store immutable data in an observable field.
observable.shallowLike observable.ref but for collections. Any collection assigned will be made observable, but the contents of the collection itself won't become observable.
observable.structLike observable, except that any assigned value that is structurally equal to the current value will be ignored.
actionMark a method as an action that will modify the state. Check out actions for more details. Non-writable.
action.boundLike action, but will also bind the action to the instance so that this will always be set. Non-writable.
computedCan be used on a getter to declare it as a derived value that can be cached. Check out computeds for more details.
computed.structLike computed, except that if after recomputing the result is structurally equal to the previous result, no observers will be notified.
trueInfer the best annotation. Check out makeAutoObservable for more details.
falseExplicitly do not annotate this property.
flowCreates a flow to manage asynchronous processes. Check out flow for more details. Note that the inferred return type in TypeScript might be off. Non-writable.
flow.boundLike flow, but will also bind the flow to the instance so that this will always be set. Non-writable.
overrideApplicable to inherited action, flow, computed, action.bound overridden by subclass.
autoActionShould not be used explicitly, but is used under the hood by makeAutoObservable to mark methods that can act as action or derivation, based on their calling context. It will be determined at runtime if the function is a derivation or action.

Limitations

  1. make(Auto)Observable only supports properties that are already defined. Make sure your compiler configuration is correct, or as work-around, that a value is assigned to all properties before using make(Auto)Observable. Without correct configuration, fields that are declared but not initialized (like in class X { y; }) will not be picked up correctly.
  2. makeObservable can only annotate properties declared by its own class definition. If a sub- or superclass introduces observable fields, it will have to call makeObservable for those properties itself.
  3. options argument can be provided only once. Passed options are "sticky" and can NOT be changed later (eg. in subclass).
  4. Every field can be annotated only once (except for override). The field annotation or configuration can't change in subclass.
  5. All annotated fields of non-plain objects (classes) are non-configurable.
    Can be disabled with configure({ safeDescriptors: false }) {🚀☣️} .
  6. All non-observable (stateless) fields (action, flow) are non-writable.
    Can be disabled with configure({ safeDescriptors: false }) {🚀☣️} .
  7. Only action, computed, flow, action.bound defined on prototype can be overridden by subclass.
  8. By default TypeScript will not allow you to annotate private fields. This can be overcome by explicitly passing the relevant private fields as generic argument, like this: makeObservable<MyStore, "privateField" | "privateField2">(this, { privateField: observable, privateField2: observable })
  9. Calling make(Auto)Observable and providing annotations must be done unconditionally, as this makes it possible to cache the inference results.
  10. Modifying prototypes after make(Auto)Observable has been called is not supported.
  11. EcmaScript private fields (#field) are not supported. When using TypeScript, it is recommended to use the private modifier instead.
  12. Mixing annotations and decorators within single inheritance chain is not supported - eg. you can't use decorators for superclass and annotations for subclass.
  13. makeObservable,extendObservable cannot be used on other builtin observable types (ObservableMap, ObservableSet, ObservableArray, etc)
  14. makeObservable(Object.create(prototype)) copies properties from prototype to created object and makes them observable. This behavior is wrong, unexpected and therefore deprecated and will likely change in future versions. Don't rely on it.

Options {🚀}

The above APIs take an optional options argument which is an object that supports the following options:

  • autoBind: true uses action.bound/flow.bound by default, rather than action/flow. Does not affect explicitely annotated members.
  • deep: false uses observable.ref by default, rather than observable. Does not affect explicitely annotated members.
  • name: <string> gives the object a debug name that is printed in error messages and reflection APIs.
  • proxy: false forces observable(thing) to use non-proxy implementation. This is a good option if the shape of the object will not change over time, as non-proxied objects are easier to debug and faster. This option is not available for make(Auto)Observable, see avoiding proxies.

Note: options are sticky and can be provided only onceoptions argument can be provided only for target that is NOT observable yet.
It is NOT possible to change options once the observable object was initialized.
Options are stored on target and respected by subsequent makeObservable/extendObservable calls.
You can't pass different options in subclass.

Converting observables back to vanilla JavaScript collections

Sometimes it is necessary to convert observable data structures back to their vanilla counterparts.For example when passing observable objects to a React component that can't track observables, or to obtain a clone that should not be further mutated.

To convert a collection shallowly, the usual JavaScript mechanisms work:

const plainObject = { ...observableObject }const plainArray = observableArray.slice()const plainMap = new Map(observableMap)

To convert a data tree recursively to plain objects, the toJS utility can be used.For classes, it is recommended to implement a toJSON() method, as it will be picked up by JSON.stringify.

A short note on classes

So far most examples above have been leaning towards the class syntax.MobX is in principle unopinionated about this, and there are probably just as many MobX users that use plain objects.However, a slight benefit of classes is that they have more easily discoverable APIs, e.g. TypeScript.Also, instanceof checks are really powerful for type inference, and class instances aren't wrapped in Proxy objects, giving them a better experience in debuggers.Finally, classes benefit from a lot of engine optimizations, since their shape is predictable, and methods are shared on the prototype.But heavy inheritance patterns can easily become foot-guns, so if you use classes, keep them simple.So, even though there is a slight preference to use classes, we definitely want to encourage you to deviate from this style if that suits you better.

Creating observable state · MobX 🇺🇦 (2024)
Top Articles
Latest Posts
Article information

Author: Rueben Jacobs

Last Updated:

Views: 6073

Rating: 4.7 / 5 (57 voted)

Reviews: 88% of readers found this page helpful

Author information

Name: Rueben Jacobs

Birthday: 1999-03-14

Address: 951 Caterina Walk, Schambergerside, CA 67667-0896

Phone: +6881806848632

Job: Internal Education Planner

Hobby: Candle making, Cabaret, Poi, Gambling, Rock climbing, Wood carving, Computer programming

Introduction: My name is Rueben Jacobs, I am a cooperative, beautiful, kind, comfortable, glamorous, open, magnificent person who loves writing and wants to share my knowledge and understanding with you.