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?