In this article, I will describe a structured approach to using MobX that can help simplify the development process. The code will not be described here, only a description of the approach to use. Links will be provided to the code. And I beg you to look at the examples that I am attaching. In them it will be possible to visually see all the advantages of the described architecture.
It will also be important to mention that in order to fully understand what is described in the article, you need to be familiar with the Observable/Observer, MVVM and DI patterns.
A lot of people already know about MobX technology itself. According to npmjs.com, this package averages about 850,000 downloads per week. Its main competitor can be considered the Redux library, and judging by the same data, it is downloaded on average 8 times more often. Such popularity of Redux against the backdrop of a much more convenient MobX is difficult for me to accept, since I am an ardent supporter of this library. Therefore, in this article I would like to describe why MobX is so good and how you can make it even more convenient.
In general, there are already quite a few articles describing the advantages of MobX in comparison with Redux, but for those who are not familiar with them, now I will describe them as briefly as possible:
MobX doesn’t require you to write boilerplate code;
There can be many stores in MobX, which means they can be logically separated;
MobX is much easier to understand and learn;
Typing in MobX is much easier to describe and use.
But! There’s a pretty big “But!” I do not like the approach of MobX itself, which is described for the interaction of this library with React. In the examples that MobX developers describe, it is proposed to create some object – a store – in which updated information should be stored. The problem is that this is too vague a representation that does not describe the possibilities of more complex interaction, for example, when components are nested or when using one store with another.
Level One: Better MVVM
At this point, all introductions end. Next, I will talk about improvements that can improve interaction.
The interaction between View and ViewModel can be pumped a little:
The ViewModel object itself can be created at the time of the first rendering of the View. So, in the constructor of the ViewModel, you can write code that will work before mounting the View. In addition, the computer memory does not have to store unused information.
Props passed to View can be passed to ViewModels. At the same time, it is convenient to make the field in which the props of the ViewModel are stored observable, which will automatically track their change.
When nesting one View into another, you can pass a link to the parent ViewModel to the child ViewModel.
If necessary, the ViewModel can describe a method that is launched when the View is mounted and unmounted.
For clarity, let me describe it in the picture
ViewModel1passing her their props.
View2 is a child element
View1 (optionally directly) in the virtual DOM.
ViewModel2passes it a link to
ViewModel1 and your props.
View does not have to be an observer component. Its main task is to initialize the object of its ViewModel, pass its props to it and, if necessary, call the callbacks of its life cycle.
Another improvement in MVVM is the introduction of the concept childview – some component inside the View, which also has a link to the ViewModel. ChildView does not initialize a new ViewModel and does not pass its own props to it. The ChildView also doesn’t have to be an observer component.
In this example,
View have 3
ChildView and each has a link to the ViewModel. Of the ChildViews presented, only the lower left is an observer component. The rest use static fields and/or methods of the ViewModel.
Level two: Services
The described approach can still have its drawbacks. For example, why store some general information for the entire application in some ViewModel? In that case it is logical to make some separate class. I propose to call such a class a service.
The service should store general information, for example, information about the user – the status of his authorization and a list of allowed actions; or methods: creating a pop-up notification, switching between pages, etc. A service can also be a store, that is, store observable fields.
For a bunch of services and view models, the DI pattern is ideal. For those unfamiliar with its concept, I recommend reading about it separately. But in short, DI is good because it is enough to specify the type of the desired class in the constructor in the class, and the built-in DI mechanisms themselves initialize the desired object.
There are not very many restrictions on services. If, in general, a ViewModel can have a link to 1 other ViewModel – the parent – then it can use as many services as necessary. Moreover, some services can freely use other services.
In DI implementations, there is often a division into Singleton and Transient classes. And in general, I recommend making ViewModel Transient a class, and a Singleton service.
Level Three: Better Model
But in my experience, I have found that forms often have repetitive logic for validating fields and keeping track of whether any of the fields have been changed. Therefore, for such forms, I suggest using a more advanced model.
In such models, in addition to information about the fields – their type and, possibly, default values - you can store meta-information about the fields. In additional decorators, you can specify how and whether certain fields should be validated in principle; and also indicate the change of which fields you want to track.
Having played a little with this concept, I have described the essence of such a model. At “pumped” models can be the following advantages:
The pumped model automatically tracks changes in its fields. Moreover, if during initialization about the model, the field field was equal to an empty string, then became equal to any other string, and then empty again, the model will say that the data did not change.
The model is able to keep track of which fields are not currently valid. Moreover, not only field values, but the entire object as a whole can participate in the validation process.
A pumped model is able to inherit from another. At the same time, in the child model, you can overwrite the meta-information of the parent model and / or create new fields.
A model field can be another model, an array of models, or a dictionary of model values. In this case, the parent model has a mechanism to control the change and check the validation of nested models.
In the end, I would like to list the advantages of the described architecture:
Using this development approach, you can structure the approach to handling application state. At the same time, the approach itself uses exclusively existing patterns.
This approach does not need to write boilerplate code and there are no typing issues.
The data can be logically divided into several store classes, so that the general ones (located in the services) can be used throughout the project, and the private ones (located in the ViewModel) can only be used in the necessary parts.
Views can only consist of JSX code, without using any hooks.
Working with forms is reduced to a qualitative description of models.
In addition, I can also say that developers familiar with Vue will find it much easier to switch to React with this architecture than with Redux, because MobX in the MVVM format has a lot of similar concepts.
In the article, I only dealt with the description of the entities of the architecture, and how they should interact with each other. And although I attached very, it seems to me, useful links with code, I did not leave my comments on it. Therefore, a little later I will write 1-2 more articles in which I will describe useful use-cases of the described architecture. And if I see interest in the comments, I will do it as soon as possible.