Skip to content

Migrating from mobx 3 to mobx 4

Eyal Ofri edited this page Dec 30, 2018 · 18 revisions

General overview of the changes

The migration guide is pretty long and big. Sorry for that :-). The good news: most of the things you have probably never heard of. But the list should be complete. MobX 4 will detect most incorrect / deprecated api usages at runtime, so the easiest approach is to just upgrade, and use this list for trouble shooting. If you have TypeScript (or Flow) enabled in your project, most deprecated api's will already flagged at compile time.

MobX 4 dropped all api's that were already deprecated in MobX 3. So make sure you don't have any deprecation warnings before upgrading :)

The extras namespace is removed, and all methods are now exposed at top level in the package. This allows for better tree shaking by tools like rollup or webpack.

MobX 4 introduces separation between the production and non production build. The production build strips most typechecks, resulting in a faster and smaller build. Make sure to substitute process.env.NODE_ENV = "production" in your build process! If you are using MobX in a react project, you most probably already have set this up. Otherwise, the idea is explained here.

The observable.* api has been simplified and made more uniform. Also, decorators can now be used both with decorator syntax enabled and disabled. For 80 - 90% the api will remain the same as in MobX 3, but for edge cases you will have to change some stuff.

Most api's are simplified. Many api's took for example optionally a name as first argument, and some addition flags as last arguments. Almost all api's have moved to required arguments up front, and then an object with optional flags. This makes the api simpler and easier to read.

For typescript users, tslib should be set to es6 in the config (compilation target can still be ES5)

Things that have been removed

Note; many things that were removed were removed to make the api surface smaller. If you think some feature shouldn't have been removed, feel free to open an issue!

shareGlobalState has been removed. There are enough ways to setup a project with shared MobX properly :).

Passing a context (this) argument to autorun, reaction, runInAction etc. etc. is no longer supported. Use arrow functions instead.

isModifierDescriptor, isStrictModeEnabled, extras.spyReport, extras.spyReportEnd, extras.spyReportStart, extras.isSpyEnabled no longer exist.

whyRun has been removed, use the neat trace feature instead.

The default export of mobx is no longer exposed. This means you can no longer do import mobx from "mobx". Instead, import things explicitly: import { observable } from "mobx". (Or use import * as mobx from "mobx" and kill tree-shaking)

Things that just have moved..

Many things have moved from the extras namespace. If they are prefixed with _ they are still unofficial api (but used for testing for example). Unprefixed functions are now officially supported.

Old api New api
useStrict(boolean) configure({ enforceActions: boolean })
expr Now part of the mobx-utils package
createTransformer Now part of the mobx-utils package
map(values) observable.map(values)
extras.allowStateChanges _allowStateChanges
extras.deepEqual comparer.structural
extras.getAdministration getAdministration
extras.getAtom getAtom
extras.getDebugName getDebugName
extras.getDependencyTree getDependencyTree
extras.getGlobalState _getGlobalState
extras.getObserverTree getObserverTree
extras.interceptReads _interceptReads
extras.isComputingDerivation _isComputingDerivation
extras.isolateGlobalState() configure({ isolateGlobalState: true })
extras.onReactionError onReactionError
extras.reserveArrayBuffer(number) configure({ arrayBuffer: number })
extras.resetGlobalState _resetGlobalState
extras.setReactionScheduler(fn) configure({ reactionScheduler : fn })

Things that need to be expressed differently

Modifiers are dead. Long live decorators

The decorators observable.ref, observable.shallow, observable.deep, observable.struct can no longer be used as functions, instead, we made the api more consistent by always using these decorators really as decorators. For example, observable.object and extendObservable now support a decorators parameter.

One should use the decorators parameter where in Mobx 3 decorators were used as object modifiers. For example:

// MobX 3
const myObject = observable({
  name: "Michel",
  profile: observable.ref(someDataFetchingPromise)
})

// MobX 4
const myObject = observable({
  name: "Michel",
  profile: someDataFetchingPromise
}, {
  // specify the decorators. 'observable' is the default, so we only mention 'profile':
  profile: observable.ref   // n.b.; no ()!
})

The advantage is that the usage between @decorator and decorator is now consistent, and they can always be called in the same way and with the same arguments. For example, the following examples now achieve all the same, and you will notice it is much easier to switch between different syntaxes or ways of creating objects:

class Todo {
    @observable title = "test"
    @observable.ref promise = somePromise

    @computed get difficulty() {
        return this.title.length
    }
    @computed({ requiresReaction: true })
    get expensive() {
        return somethingExpensive()
    }

    @action setTitle(t) {
        this.title = t
    }
    @action.bound setTitle2(t) {
        this.title = t
    }
}

// observable.object takes a second 'decorators' param, specifying which decorators need to be applied.
// defaulting to `observable` for omitted fields (or `computed` for getters)
const todo = observable.object({
    title: "test",
    promise: somePromise,
    get difficulty() {
        return this.title.length
    },
    get expensive() {
        return somethingExpensive()
    },
    setTitle(t) {
        this.title = t
    },
    setTitle2(t) {
        this.title = t
    }
}, {
    // title: observable can be omitted, it is the default when using observable.object / extendobservable
    ref: observable.ref,
    expensive: computed({ requiresReaction: true }),
    setTitle: action,
    setTitle2: action.bound
})

// Maybe you have classes, but no decorator syntax enabled. Don't worry, with MobX 4 you don't have to fall back to
// `extendObservable` in the constructor! Instead, just declare the fields and use `mobx.decorate` to enhance the prototype:
class Todo {
    title = "test"
    promise = somePromise

    get difficulty() {
        return this.title.length
    }
    get expensive() {
        return somethingExpensive()
    }

    setTitle(t) {
        this.title = t
    }
    setTitle2(t) {
        this.title = t
    }
}
decorate(Todo, {
    title: observable,
    ref: observable.ref,
    expensive: computed({ requiresReaction: true }),
    setTitle: action,
    setTitle2: action.bound
})

The observable factories have been cleaned up

Old api New api Notes
observable(primitive value) observable.box(primitive value) As decorator @observable still works the same (also for primtive values). If the value is a plain object, array, or ES6 Map, observable(value) will keep working as is
observable(class instance) observable.box(class instance) As decorator @observable still works the same (also for primitive values). If the value is a plain object, array, or ES6 Map, observable(value) will keep working as is
observable.shallowArray(values) observable.array(values, { deep: false })
observable.shallowMap(values) observable.map(values, { deep: false })
observable.shallowObject(values) observable.object(values, {}, { deep: false }) Note the empty object as second argument, which can contain decorators (see above)
extendObservable(target, props, moreProps, evenMoreProps) extendObservable(target, {...props, ...moreProps, ...evenMoreProps}) extendObservable no longer accepts multiple bags of properties. Rather, merge then first, then extend the target.
extendShallowObservable(target, props) extendObservable(target, props, {}, { deep: false }) Note the empty object as third argument, which can contain decorators (see the release notes)
@computed.equals(compareFn) @computed({ equals: compareFn })
autorunAsync(fn, delay) autorun(fn, { delay: delay })
autorun(name, fn) autorun(fn, { name: name })
when(name, predicate, effect) when(predicate, effect, { name: name })
reaction(name, expr, effect) reaction(expr, effect, { name: name })
isComputed(thing, prop) isComputedProp(thing, prop) isComputed(thing) still works as is
isObservable(thing, prop) isObservableProp(thing, prop) isObservable(thing) still works as is
new Atom(name, fn, fn) createAtom(name, fn, fn) The Atom class is no longer exposed from the package directly. Note that there are now global functions onBecomeObserved and onBecomeUnobserved than can be used on any MobX observable, so in many cases you might not need custom atoms at all.
computed(fn, { struct: true }) / computed(fn, { compareStructural: true }) computed(fn, { equals: comparer.structural })

Other things that have changed

MobX now requires Map to be globally available. Make sure to polyfill them in older browser versions. (In practice, IE 10 and older). When using observable.map() and serializing, you may need to update your code. observableMap.toJS() now returns a shallow instance of Map. To keep the behavior of creating a plain JavaScript object, use observableMap.toJSON()

extendObservable can no longer redeclare existing properties. It can only introduce new properties. Use set instead to introduce new properties or update existing ones

All iterables no longer create an array as iterator, but only a real iterator, to be more closely aligned to the official specs. So previously observableMap.values() would return an array and iterator, but now it will only return an iterator. So you can no longer do observableMap.values().map(fn). Instead, use Array.from(observableMap.values()).map(fn) or mobx.values(observableMap).map(fn). The affected iterators are: 1. The default iterator for observable arrays. 2. The default iterator for observable maps. observableMap.entries(), observableMap.keys() and observableMap.values().

The data passed to spy handlers have been changed slightly.

Calling reportObserved() on a self made atom will no longer trigger the hooks if reportObserved is triggered outside a reactive context.