I am working on the SPA which is becoming fat and complex due to its (proper and necessary) requirement and difficulty. I want to manage the problems before they become too big to deal with.
In this article, I try to re-introduce the rough structure to the frontend architecture, between the UI components and API access objects, to divide them and ease the complexity. I hope this helps you to organize the problems, to review the code, and to avoid "unnecessary" consideration.
I observed that the practice such as layering, UI component classification, and component composition is widely spread among the community today for the middle size components. By putting the responsibility of the visual to the Presentational Component, the logic/data process is well managed in the Container Component. It reduces the chance of mixing the visual and logic/data process, which leads it to the "legacy" code.
On the other hand, I sometimes feel that there is no good rule nor guideline in the codebase between the UI components and the API access objects. When the logic/data process becomes fat, we would use the library called the Store such as Redux + async plugin, MobX, and (your favorite library) to ease the complexity. Though with that, the store becomes fat/complex too unless there is a good rule. These fatness/complexity usually would not be recognized until the new members point out it or they get confused. It is also the high congnitive load for whom understands codebase well, due to the team mates' change.
I assume that the one of the cause of fatness/complexity, in the large codebase between the UI components and the API access objects, is the mix of the 2 responsibilities in the name of Store.
- The responsibility of external data access and caching
- Accessing API to fetch data and holding it. The Repository.
- Its data will not change unless executing write operations.
- The responsibility of storing UI states and managing them
- A global store which its data keep over the page transition.
- Its data will change anytime by user's UI operation or some machine events.
For example, we assume there is a search page with "Search" button at the center and the facet view at the left, the store with the first responsibility should have the search condition data which indicates the current search result, and the one with the second responsibility should have the search condition draft which indicates the current UI state.
I remember that the past configuration of Redux + async plugin combination usually tends to mix both of them in the same store/module. If they are separated into these reducers, it may be okay.
From here, I call the store with the first
responsibility as Repository and the another one as "UI Store".
There are several technologies, of course, we just use them considering the kinds above. Though there is a right tech in the right place, I think we can choose them freely to the 2 kinds of "Store" (including the choise of the same tech).
- Hook based store
useReducerin the component
- Custome hook
- The global store with "constate" or self-made Redux-like store.
- vanilla (plain)
- async plugin such as redux-thunk
- Data access class (including MobX)
- GraphQL library with caching feature
- Data resource of React Concurrent mode
Example: Using a data resource of React Concurrent mode as the Repository and using a custome hook as the "UI Store".
Separate Presentation "System"1
When the store layer becomes fat/complex and there are multiple different responsibilities, concerns, or systems in it, they should be separated; draw the dividing line between the Repository and the "UI Store". This is not a new idea. In the application server architecture, we usually follow the rule that separates the presentation layer (the view in MVC) from others. With the exception of the interface/contract, the Repository should not know what happens in the Presentation System, and vice versa.
It does not mean better or not, but I just list up possible implementations2. In Pattern 1, "UI Store" is the bridge between the Repository and the Presentation System. In Pattern 2, the Container Component, and stateless hook called in it, combines the data flow from the Repository to the "UI Store".
Though there are other perspectives of dividing codebase, I believe that only few methods conflict with this Presentation System separation. Of course it requires the kind of dependency injection to avoid the conflicts as usual. By separating the Presentation System as the first step, I believe, we can still easily separate the codebase with other perspective and the problem becomes smaller by removing the typical mix of the responsibility first.
Using reactive object such as Rx
Do it outside of the Presentation System. We should depend on the reactivity only provided by the view library (React, Vue, etc.) in the inside of the system.
Separating Command/Query like frontend CQRS
Do it outside of the Presentation System. We should consider the data flow separately with Flux, in the system.
Concentric layered architecture3
Do it outside of the Presentation System. The presentation system corresponds to the outer layer(shell) since it is the presentation4.
DDD in frontend
Do it outside of the Presentation System. Maybe the system depends on either the primitive values or DDD domain models and/or usecases5.
Various component design patterns
It should be closed in the Presentation System. I have not seen the component design pattern which crossing the system boundary.
It should be closed in the Presentation System.
I described that we can divide the store into the 2 parts, the Repository and the "UI Store", and the codebase will be simpler if separating the Presentation System from others first.
It's too large to call it as a "layer", so I call it as a "system". Moreover, just using presentation "layer" seems like that its target is only around the Presentational Component. ↩
Initially I started writing this article with Pattern 1 in mind. However, I currently feel that Pattern 2 will be the mainline through the layer division of Container Component in the future. ↩
Such as Hexagonal Architecture, Clean Architecture, and Onion Architecture. ↩
I believe, however, the most important part is the Presentation System in frontend. ↩
To be honest, I do not have an experience of DDD in frontend. This is just a wild guess. ↩