3
votes

I'm trying to migrate my TypeScript / create-react-app application from MobX 5 to MobX 6. In the official migrating guide ( https://mobx.js.org/migrating-from-4-or-5.html#upgrading-classes-to-use-makeobservable ) they suggest the following:

Remove all decorators and call makeObservable in the constructor and explicitly define which field should be made observable using which decorator.

But they don't say how to do this with a TypeScript app. The examples given are always using plain JavaScript and React.RenderDOM directly.

Here is the beginning of a simplified example - a typical class that I need to migrate:

@observer
export default class LoginPage extends React.Component<ILoginPageProps, {}> {
    @observable private email: string = process.env.REACT_APP_DEFAULT_LOGIN_EMAIL || "";
    @observable private password: string = process.env.REACT_APP_DEFAULT_LOGIN_PASSWORD || "";

    constructor(props: ILoginPageProps) {
        super(props);
        this.state = {};
    }

The "official" way to do it is to add the makeObservable call to the constructor, and set annotations there. I have managed to refactor an actual class by hand, and it works:

const LoginPage = observer(class LoginPageClass extends React.Component<ILoginPageProps, {}> {
    private email: string = process.env.REACT_APP_DEFAULT_LOGIN_EMAIL || "";
    private password: string = process.env.REACT_APP_DEFAULT_LOGIN_PASSWORD || "";
    private catpchaKey: number = 0;
    private catpchaToken: string | null = null;
    private errormessage: string = "";
    private overloaymessage: string = "";
    private checking: boolean = false;

    constructor(props: ILoginPageProps) {
        super(props);
        this.state = {};
        makeObservable<LoginPageClass,"email"|"password"|"catpchaKey"|"catpchaToken"|"errormessage"|"overloaymessage"|"checking"|"loginEnabled"|"verifyCaptchaCallback">(this, {
            email: observable,
            password: observable,
            catpchaKey: observable,
            catpchaToken: observable,
            errormessage: observable,
            overloaymessage: observable,
            checking: observable,
            loginEnabled: computed,
            verifyCaptchaCallback: action
        })
    }

    get loginEnabled(): boolean {
        return !!this.email && isValidEmail(this.email) && !!this.password && this.password.length > 4;
    }

    verifyCaptchaCallback = (token: string) => {
        this.catpchaToken = token;
    }
    
    // More methods and code here...
}
export default LoginPage;

There are unspeakable number of problems with the refactored code. Just to name a few:

  • To produce type safe code, the name of every observable property must be typed three times. For example, the name "email" appears in the field declaration, in the type declaration of the makeObservable call and inside the annotations parameter of the makeObservable call. Some classes can contain up to a hundred properties, computed properties and actions.
  • This code cannot be refactored with standard refactoring tools anymore, because the field names appear in uncorrelated places. There is no IDE that can correctly refactor the name of a field or a MobX action method.
  • The name of all property getters and actions must be triplicated, and they also can't be refactored easily.
  • The information that marks a method as a "bound action" is separated from the method definition. If you take a quick look at a method definition, then you are unable to tell if that is a MobX action or not.
  • Same stands for property getters - they might be computed, but you have to scroll up to another place to find out.

Another thing that they suggested is to use npx mobx-undecorate - but it does not work with TypeScript. I tried that, and it just messed up my code. Ended up in an infinite loop eating up all of my CPU, printing endless tracebacks etc. In the end it produced code something like this:

export default const LoginPage = observer(class LoginPage extends React.Component<ILoginPageProps, {}> {
    private email: string = process.env.REACT_APP_DEFAULT_LOGIN_EMAIL || "";
    private password: string = process.env.REACT_APP_DEFAULT_LOGIN_PASSWORD || "";

    constructor(props: ILoginPageProps) {
        super(props);

        makeObservable<LoginPage, "email" | "password">(this, {
            email: observable,
            password: observable,
        });

        this.state = {};
    }

This is ridiculous, not even valid TypeScript code. (The "export default const" is simply invalid syntax.) It replaced my class with a function call, meanwhile created a name collition. The name of the function and name of the class are the same.

As far as I can see, the recommended approach makes everything much worse.

So my question in the end: is there a way to refactor React/MobX/TypeScript code from MobX 5 to MobX 6, without making the code a huge unbearable mess? Did anyone find a way to achieve this?

1
This question is not a duplicate of stackoverflow.com/questions/61713995/… - that question is about store injection. - nagylzs

1 Answers

1
votes

Well, it seems like you have tried first of three ways to migrate and did not liked it. But this way is only recommended when:

This is the recommended approach if you want to drop decorators in your code base, and the project isn't yet too big.

But I agree that this way of writing stores is indeed quite hard with TS, lots of duplication and etc.

If you want continue to use decorators just keep them where they are, and and call makeObservable(this) in the constructor, just like the docs says:

Leave all the decorators and call makeObservable(this) in the constructor. This will pick up the metadata generated by the decorators. This is the recommended way if you want to limit the impact of a MobX 6 migration.

There is also a third way, which I my opinion, is the best way right now without any bloat:

Remove decorators and use makeAutoObservable(this) in the class constructor's.

I just quoted the docs that you have already read and I can't suggest any other way to migrate. If you have really big project with decorators then use the second way at first, then gradually migrate to the third if you like it.