The text/sheets
Data flow has always been a problem in the front end. Every year, we see a large number of new wheels of data flow coming out 😂. Regarding data flow, our project has accumulated a set of best practices
Background:
In the old Done project, the code complexity was so high that it was “all in one piece” and the technical debt was so high. Due to the “complexity” of the old code, even a simple function required two to three times more work hours than normal. As is the case with the picture below.
A careful analysis of the existing business results in the following business characteristics:
-
Strong domain (e.g., project/file/team/user domain, where many components call methods in one domain at the same time, mute/like/move the project…)
-
There are many and complex single pages, too many components, and too much communication between multi-layer nested components.
-
The business has been achieved from 0 to 1, and is now in the stage from 1 to N. At this stage, a large number of product interaction adjustments will be made based on user suggestions to polish the quality. The demand quantity increases, the front end changes greatly, the front end manpower bottleneck is big.
Based on the above business characteristics, we analyze the problems in the current project:
-
Too much template code affects development efficiency and maintainability
-
The data flow spirals into mesh calls (strongly coupled), and the code complexity increases dramatically
-
Data globalization
-
Conversion of raw data to presentation data
-
UI and data logic coupling, low reuse
Based on the above characteristics and problems, we have the following demands:
-
A simple and efficient
-
Reject template code
-
Reduce code complexity (reduce linkage impact)
-
Improved reusability
-
Reuse the UI/logic layer to the greatest extent
-
Multiplexed data conversion
Based on the above questions and analysis, the new architecture diagram & technology selection will be derived step by step.
Problem analysis:
I. Low reuse degree of UI and data logic coupling & conversion between original data and display data
The original overall architecture is as follows: Store and view layer are mixed together to process user behavior and business logic, with high coupling degree and low reuse rate.
Architecture evolution:
Improvement:
The independent Store layer encapsulates the data related to the application program and business logic as well as the processing method of the data. The logic is written independently in this layer, which supports the reuse of one logic by multiple components. It is independent from the view layer, and one logic can be reused by multiple components.
As we all know, layering is for decoupling. If the Store layer is changed, there is no need to change the view layer, just the Store layer. This way, there are fewer changes, and it also improves development efficiency & maintainability.
However, we also encountered some scenarios where there were too many changes to be made when fields were changed in the back-end interface. The same interface is displayed in different places. Due to multi-person maintenance, it will undergo multiple data conversion and finally map to the front-end interface. Therefore, we added an API anticorrosion layer to deal with the data conversion between the front and back interfaces, so that once the data structure changes, we only need to modify in the API layer, and do not need to pay attention to multiple interface components
Improvement:
The independent API layer connects the back-end service with the front-end service. If the back-end interface changes, it can be modified uniformly in this layer without modification of multiple codes, so as to facilitate data inspection in this layer and prevent the background from returning wrong fields, etc
Through the above layer, the above two problems are easily solved.
Second, data flow mesh call
We layered Store and view, but as the project iterated, the data flow was called in a mesh.
An example: Mute a project
In the old code, the workstation would call the data in the team, modify the data in the team, and even after the interface callback, it would modify the data related to the mute interface everywhere.
This causes A problem. Since this is A globally shared Redux, we call this method at A, where the B interface is updated (where B does not appear on the user interface). In other words, we need to pay attention to B,C,D as well as the action at A. These places (actually, we don’t need to focus on other places)
We know that layering decouples and is very effective at reducing complexity. Can we also layer stores and constrain them to call each other? Redux’s data flow is centralized, while Mobx naturally supports polymorphism.
Architecture evolution:
After refactoring, stores at the same level cannot call each other. If they need to be called, this means that the store has to be promoted to the next level (more on that below). With this approach, the entire store layer is clear and does not become more complex with business iterations.
After refactoring, we can do this with just a few lines of code:
Each view component (A/B/C/D) takes the result of the call to mute and updates the interface itself.
In this way, instead of having to focus on N places, we now have to focus on only one place, greatly reducing the complexity of the code.
Third, data globalization
Here’s an example:
We will use state promotion to solve the problem of sharing some state between non-parent components. However, if the nested components are too deep, then the components passing through the middle will help pass the props, and if the parameters need to be passed or the props need to be increased, A, B, and * n of the intermediate components need to be modified.
At this point, we will put these states into the global REdux. However, this will lead to other problems. For some temporary states, such as the common scene in the middle and background: Component A controls the hidden state of the popup component E on the same layer, and these states do not need to be saved for users, and are one-time.
Because there are too many components in the middle, we put it in Redux. Over time, there are more and more actions in Redux. Over time, we need to make sure that each change affects the other components as well (in fact, this action is only used in this module, not the other modules).
For Redux, data flow management is centralized. Look at most of the background products, the need to share global data is not much, generally only user information. In this context, we look at Mobx, which naturally supports multiple stores. So how do you design a store?
High cohesion is certainly a good thing for improving the maintainability of a project, but it can also cause problems if granularity is not controlled. For example, we strictly follow the React official guidelines: if multiple components interact, state (i.e., data) is maintained on the minimal convention parent of those components
Then according to this classification, our program will appear nodes into ten hundreds of convention, iterative forward, as the project was A, B share, back into A, B, C, sharing, the largest convention node need upward again, in the case of collaboration, too much improvement and change is easy to cause the instability of the project, so, We divide the following three layers of stores, and the store with the smallest granularity is the module store.
The Store diagram mapped to a specific project looks like this:
Domain model DDD
How do we design the Store? We used domain-driven design, also known as DDD.
Why do YOU need DDD?
-
In the traditional front-end development process, the front end and the business are connected through visual draft, and when the visual draft changes, it means a lot of modification costs. At present, the product has reached the stage 1 ~ N, and the change of visual draft requirements is inevitable.
-
A complex product is a collaboration of many people. If everyone designs the Store according to the visual draft, the logic of repetition will be written more and more, and the technical debt will become bigger and bigger.
-
Joint business
If we use DDD, for example, we abstract a domain model “file”, where we store operations related to the file: “change the file name, delete the file, move the file…” With such a stable domain model, the view layer only needs to implement visual draft and assemble business logic, with strong flexibility, just like building blocks, the underlying domain model does not need to change, only need to change interactive changes or views. Greatly improve the development efficiency and maintainability.
For example, as the project iteration, about documents related operations: delete, mobile etc. These are the precipitation in the domain model, if the product changes, delete the entry changed, or is added a new delete entrance, that we only need to modify the view components, and then call where you need to call the corresponding areas of the action
The boundaries between stores are clearer by domain.
Note: DDD is not a framework, but an architectural idea, so the front-end needs to be refined and the Store designed before development.
Settled areas of the project
Improvements: According to the Official Mobx guidelines, in addition to the loose state of the Page, there are also some common domain models based on the overall business characteristics, which can be injected into the corresponding Page Store according to the needs of different pages.
The concept of domain-driven design has been in our business for several months. Students who participated in the development all agreed that, in the case of a large bottleneck of front-end manpower, students in the background also participated in writing Store. Store and view were simultaneously developed in parallel, from serial development to parallel development, which greatly improved the overall development efficiency.
Technology selection
Having written so much, the new system architecture requires the following features to make the system go further and faster:
-
Reasonable layering, UI/logic separation
-
Granularity refinement of complex project Store is important, domain model DDD
-
Reject template code to improve development efficiency
-
Future-oriented & compatible with old code — hook & class support
-
Better TS support
We want to have these features and compare them one by one.
unstated-next
-
I didn’t know how many providers to write, the writing method was too flexible, and organizing into domain models required a lot of modification (I tried it in a small project, but finally gave it up as the business became more and more complex).
-
The old business logic needs to be reformed. Once class is encountered, it will go back to the old writing method. It is impossible to stop and transform hook one by one in the rapid business iteration.
redux
-
Lots of template code
-
Overly flexible Redux calls (mesh calls)
-
TS support renovation cost is large
Why mobx?
-
✅ Little/no template code
-
✅ future-oriented & compatible with old code – supports Hook & class
-
✅ makes it easy to layer businesses and design domain models
-
✅TS supports 0 cost
-
✅ naturally supports polymorphic stores, making decentralization more convenient
-
✅ needs to display DI, which solves the mesh call problem
Mobx’s shortcomings have been widely discussed in the industry, including the following:
-
State can be changed at will — Solution: Enable strict mode restrictions and can only be changed in Store, not in view components
-
Hooks are not supported, mobx-React-Lite supports them
For all these reasons, it would be much more expensive to rebuild or modify a wheel for these features than it would be to use Mobx’s power. Therefore, after investigating various data flow solutions, Mobx was chosen to support our above architecture.
conclusion
There is no best technical solution, only the best technical solution for the business. We developed a solution from one “business pain point to another” and have been running it on real projects for months with good results. If you have any questions, please discuss ~