Thursday, October 1, 2020

MobX 6

Five years of MobX

Time flies, and it has been 5.5 years since the first commit to MobX was made. In those years MobX has been adopted by well-known Software companies like Microsoft (Outlook), Netflix, Amazon and, my personal favorite, it runs in the Battlefield games by EA. Books and video courses have been written, and so have implementations in other languages.

Battlefield & MobX

Yet, since that first commit the philosophy of the MobX hasn't changed: Anything that can be derived from the application state, should be derived. Automatically.. The API hasn't changed too much since those days either, and you will find the original introduction of MobX still pretty recognizable. If React.createClass still rings a bell that is.

In contrast, the JavaScript eco-system has changed significantly over the years. TypeScript and Babel have become the de-facto standards. React went from createClass to classes to hook-based function components. Yet, relevant JavaScript proposals for observables, Object.observe and decorators have never materialized.

MobX 6 is a new major version that doesn't bring many new features, but is rather a consolidation of MobX on the current state of affairs in JavaScript. That doesn't come without a few plot twists, so if you are an existing MobX user, please read till the end!

Bye bye decorators

Let's start with the bad news: Using decorators is no longer the norm in MobX. This is good news to some of you, but others will hate it. Rightfully so, because I concur that the declarative syntax of decorators is still the best that can be offered. When MobX started, it was a TypeScript only project, so decorators were available. Still experimental, but obviously they were going to be standardized soon. That was my expectation at least (I did mostly Java and C# before). However, that moment still hasn't come yet, and two decorators proposals have been cancelled in the mean time. Although they still can be transpiled.

So why did we stop using decorators by default?

First of all, the current experimental decorator implementations are incompatible with the soon-to-be-standardized class-fields proposal. The legacy (Babel) and experimental (TypeScript) decorator implementations will no longer be able to trap class fields initializations.

Secondly, using decorators has always been a serious hurdle in adopting and advocating MobX. In Babel, it is quite fragile to set up. create-react-app doesn't support it out of the box, and many developers rightfully don't like to use non-standard features. Even though decorators have always been optional in MobX, the fact that they were prominent in the docs left many confused. Or as one MobX fan puts it:

I am guessing this choice [to drop decorators] was probably a good call. Maybe now without decorators I am hopeful I will be able to convey to people how amazing Mobx is and at least I won't hear the decorators excuse anymore. I have never seen an "@" sign scare so many people.

So, what does MobX after decorators look like? Simply put, instead of decorating class members during the class definition, instance members need to be annotated in the constructor instead, using the new makeObservable utility:

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


class TodoStore {
    @observable
    todos: Todo[] = []

    @computed
    get unfinishedTodoCount() {
        return this.todos.filter(todo => !todo.done).length
    }

    @action
    addTodo(todo: Todo) {
        this.todos.push(todo)
    }
}


class TodoStore {
    todos: Todo[] = []

    constructor() {
        makeObservable(this, {
            todos: observable,
            unfinishedTodoCount: computed,
            addTodo: action
        })
    }

    get unfinishedTodoCount() {
        return this.todos.filter(todo => !todo.done).length
    }

    addTodo(todo: Todo) {
        this.todos.push(todo)
    }
}

Admittedly, this is a slightly worse DX than before, since member and annotation are no longer co-located. But the good news is that using makeObservable doesn't require any fancy build setup. It should work everywhere out of the box.

Migrating an entire code-base from decorators to makeObservable might be challenging, so that is why we released a code-mod together with MobX 6 to do that automatically! Just run the command npx mobx-undecorate inside the folder where your source files live, and after that all decorators should have been magically rewritten! After that, make sure to update your TypeScript / babel config, and you should be good to go!

Introducing makeAutoObservable

We realize it is easier to make mistakes now that the annotations are no longer adjacent to the fields they are decorating. Hence a convenience utility has been introduced that automates the annotation process by picking sane defaults: makeAutoObservable. It will automatically pick the best annotation for every member of a class, thereby simplifying the above listing to:

class TodoStore {
    todos: Todo[] = []

    constructor() {
        makeAutoObservable(this)
    }

    get unfinishedTodoCount() {
        return this.todos.filter(todo => !todo.done).length
    }

    addTodo(todo: Todo) {
        this.todos.push(todo)
    }
}

Note that it is possible to pass a map of overrides as second argument, in case you want to use a modifier. For example: makeAutoObservable(this, { todos: observable.shallow }). Pass member: false to have MobX ignore that member entirely. (Technical fineprint: class methods will not be decorated with action, but with the new autoAction, this annotation will make methods suitable to be used both as a state updating action, or as function that derives information from state).

What I personally like about makeAutoObservable is that it plays really nice with factory functions. Which is great if you prefer to not use classes (factory functions make it is easy to hide members and prevent issues with this and new. And they compose more easily). The same store expressed as factory function will look as follows. Pick the style that suits you:

function createTodoStore() {
    const store = makeAutoObservable({
        todos: [] as Todo[],
        get unfinishedTodoCount() {
            return store.todos.filter(todo => !todo.done).length
        },
        addTodo(todo: Todo) {
            store.todos.push(todo)
        }
    })
    return store
}

Fresh docs!

Since decorators are no longer the norm, Martijn Faassen, Ε½an Gornjak and yours truly went over all the documentation. We updated all examples and significantly restructured the docs that have grown quite organically over the years. We feel the current docs are shorter, have less repetition, and better discuss common scenarios. Also, we marked all non-essential knowledge in the docs with a {πŸš€} rocket emoji, to make it clear which knowledge is optional. Hopefully it is much quicker now to find your way around in MobX!

On a similar note, we've updated all React related documentation to use function components instead of class components. And added documentation on how to use MobX with hooks, context and effects. This knowledge existed before (little has changed technically), but was scattered all over the place. As a result, we now recommend mobx-react-lite over mobx-react for (greenfield) projects that don't use class components. As a result the separate mobx-react.js.org/ website has been deprecated. All credits go to Daniel K for maintaining those two projects!

And finally, there is now a one pager MobX 6 cheat sheet πŸ‘¨‍πŸŽ“ covering all the import mobx / mobx-react(-lite) API's. (It is a great way to one-time sponser the project in an invoicable way).

Improved browser support

A probably little surprising improvement in MobX 6 is that it supports more JavaScript engines than MobX 5. MobX 5 required proxy support, making MobX unsuitable for Internet Explorer or React Native (depending on the engine). For this reason MobX 4 was still actively maintained. However, MobX 6 replaces both at once.

By default MobX 6 will still require Proxies, but it is possible to opt-out from Proxy usage in case you need to support older engines. And, as a result, it is now possible for MobX 6 to warn in development mode when features that would require proxies are used. See the documentation for more details.

import { configure } from "mobx"

configure({
    
    
    
    useProxies: "never"
})

Decorators are back!

Ok, time for the plot twist. MobX 6 stills supports decorators! The decorator implementation in MobX 6 is entirely different from the one in earlier versions, but does work with the current implementations in TypeScript and Babel. It basically provides an alternative way to construct the annotations map for makeObservable, and allows us to rewrite the first example as:

class TodoStore {
    @observable
    todos: Todo[] = []

    constructor() {
        makeObservable(this)
    }

    @computed
    get unfinishedTodoCount() {
        return this.todos.filter(todo => !todo.done).length
    }

    @action
    addTodo(todo: Todo) {
        this.todos.push(todo)
    }
}

Note that we still need to add a constructor to the class, but this time we omit the second argument to makeObservable, so that it will rely on the decorators instead. We don't recommend this set up for greenfield projects, after all decorators are still experimental, but this is a great compromise for existing code bases. Generating constructors without removing decorators is supported by mobx-undecorate as well, and can be achieved by running npx mobx-undecorate --keepDecorators.

And here is even more good news: There is a fresh decorators proposal being championed by the tireless hero Daniel Ehrenberg. I've been a bit involved in it, and the MobX use case has inspired the proposal. So the benefits of the fresh decorator implementation are that it a) solves the compatibility issue with the class fields spec discussed above, and b) it also paves the way for supporting the new decorator proposal once it standardizes! Once this happens, makeObservable(this) calls won't be needed anymore when using decorators.

Companion packages

Together with MobX 6, the following compatible companion packages will be released:

  • mobx-react-lite@3 (hooks + function components only)
  • mobx-react@6 (supports class components as well)
  • mobx-utils@6

mobx-state-tree@4 has been prepared and is technically ready. However, I'm looking for maintainers to guide that process! See this thread for more details in case you'd love to see mobx-state-tree with MobX 6 happen.

TL,DR

That is all folks! Please remember that this project is run by volunteers. If MobX has had a positive impact on your business / work, please do contribute back with time or through our open collective. The longevity of the project depends on it.

MobX 6, summarized:

  • MobX 6 standardizes MobX around the current state of affairs in JavaScript, and no longer requires non-standard transformations for decorators or class fields.
  • Existing code bases can be migrated automatically using the mobx-undecorate codemod.
  • The documentation has been almost completely rewritten.
  • The React integration now favors function components, hooks and Context.
  • Backward compatibility has been improved by opt-in support for older engines that don't support Proxy objects, like Internet Explorer and React Native.
  • Future compatibility has been improved by getting rid of the legacy decorator implementation, so that the new decorator proposal can be supported in the future.
  • MobX 4 and 5 are both superseded by version 6 and will no longer be actively maintained.
  • Further breaking changes are discussed in the changelog linked below.

Links:



from Hacker News https://ift.tt/2ENeTEg

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.