While that can be cool to link up new frameworks / libraries in our project, we often lost sight of why we are using them at all, ending of “using it for the sake of using it”.Back in days when people were making desktop / enterprise applications, many design patterns / software architecture designs have emerged. They were devised by wise engineers to untangle the complexity of information flow within their applications. Today when we are busy making web applications, this complexity doesn’t simply go away, only this time the stage changes from desktop / enterprise private servers to the browsers. Therefore, we should be able to learn from these old-and-seemingly-not-so-sexy architectural patterns to manage our front-end applications.As there are abundant coding examples and resources for learning “how-tos” of many powerful tools in the front-end community, this article should not be yet another one. Instead, our focus will be on “architecture of front-end applications”, or more explicitly, “ways of managing large number of changing parts in an app” in the richness of UI interactions nowadays. If this article can spur thoughts and discussions among this topic, it has greatly served its purpose.Before React, Angular…
This is where the writer starts the journey of web development (even before learning vanilla javascript). jQuery offers a handy way of selecting and manipulating the DOM. Say we are building a web app like Netflix. We will display a list of movies to user. We save this data in the following format:
[{id:123, title:'Ironman', savedForLater: true},
{id:456, title:'X-Men', savedForLater: false},
{id:789, title:'Aquaman', savedForLater: false},
...
]
We can loop through the array and “appendChild” to document with the relevant information included. Then for each movie we provide a button for user to click so as to toggle the savedForLater boolean field. We can bind the onClick event and update the list. To update the UI, we can either remove all the children and re-render them, or we can select the DOM child by id, create a wrapper div, appendChild with new one, remove for the old one and the wrapper. Handle-able.Next, we want to add a feature of showing a savedForLater count, a number badge on a bookmark icon button appeared in the navigation bar. (something like shopping cart !?) To keep this count up-to-date, we can bind this to the onClick event of every savedForLater button — update the list variable, update cards UI, update the count display. Not mission impossible.(There can be another approach of not creating that array variable. It is to save all the information about each movie at the data attributes of each movie DOM element. To get the count, we can loop through the elements and sum up the values at attribute like data-savedForLater)However, we can start to see that when there is a growing number of features / UI interactions, we will have to handle more and more DOM operations by ourselves, together with carefully managing any global variable (movie array, isLoading, isLoggedIn, themeColor, etc) if necessary.
Schematic Diagram of UI-State Interactions
This is where frontend frameworks such as Angular, React, Vue comes in. All these frameworks/libraries will handle DOM operations (View) for us once we update the state (Model) of an app. Update the model, change the view, sounds familiar ? Yes, let’s take a detour to architectural patterns.
Architectural PatternsMVVM Pattern, 2-way bindingWe all heard of the MVC (Model View Controller)pattern when structuring an application. In the context of SPA, we could say that Model=App State, V=Html template. For Angular and Vue.js, their patterns are closer to a variant of MVC — MVVM (Model View View-Model). In this case, each DOM element (or a set of elements) is only interested in a certain part of the model (i.e. only part of the app state). By binding each element to its responsible part of the model (View-Model layer), it can directly listen to change of (read) / update (write) its part of the model. This is also known as two-way binding.
Code snippets of Angular and Vue.js with MVVM pattern
Using this pattern, we achieved separation of concerns — separating business logic (Model) from presentational layer (View). This pattern allows fast and direct manipulation of data model, which is ideal for CRUD (create-read-update-delete)based apps.CRUD apps and Task based appsCRUD apps are for example Todo List, Employee List, etc. Rows of data items are displayed for you, and for each data item you can click “Edit” to update some attributes of that item, and then “Save” it. You also have a “Add” button to insert a new row, and a “Remove” button to delete certain row. It is like having a graphic user interface for database operation.Another kind of apps that we are also like to build is task based apps. Example apps are hotel / flight booking apps, e-commerce apps, or the “System Preferences” app at your MacBook. You go through a series of steps to get the job done. In terms of the data model / app state, the fundamental difference between CRUD apps and task based apps is that the states of latter apps will have data more than just “list of books”, for example, “currentStep”, “currentUser”, “isDarkMode”.
Excess “pathways”
If we are to use 2-way binding pattern, we create binding for each UI component with its concerned part of the model. In each of these bindings, read and write operations are strongly coupled. While this could be a good idea in CRUD apps case where each UI-Component is responsible for both the read and write operations of a certain part of the data model, in task based apps, however, it is not uncommon to have for read-only / write-only UI-Components. While it is possible to create 2-way bindings for each of these components, the excess “pathways” for mutating the same part of a data model is where the chaos of state management stems from.Here we will want to implement another design pattern CQRS (Command Query Responsibility Segregation)
, or in simple English, query (read) and command (write) should be done separately.That doesn’t mean that frameworks like Angular and Vue.js become useless. Angular service is a good example of applying CQRS pattern: you have private variables storing the items in question, public getters (query) and setters (command) as separate methods.The “Managing Data” section in Angular’s official documentation has a good illustration on this. Architectural pattern capitalizing on CQRS ? Of course there is.Event bus, 1-way bindingComponents can send messages to the event bus when an event occurs (e.g. user clicks a button). Components can also subscribe to the event bus for events they concern. The event bus will send the message to subscribers whenever it receives one.This kind of middleman-like pattern is also known as one-way binding because the component is not directly read-write bound to the model. Instead, a component subscribes to part of the model to read; and emits event to “state handler” to write rather than writing by itself. React is a library implementing 1-way binding. In other words, we need to hook up the read-write cycle by ourselves.
Example of a React component implementing one-way binding
In the above example, we could say the button is a publisher to send message to the event bus, and the h1 header is a subscriber listening to changes of the *count *part of the data model / app state.This approach of separating read-write concerns might seem very satisfying in terms of maintainability of this simple task based app (alright so your task is to pump the display number to 100… anyway), using solely this approach to tackle a large scale e-commerce app might become a nightmare. You can image that at the landing page there are numerous “state-handling functions” mutating a same piece of state (e.g. clicking Button A will toggle the value of state.A, typing text in an Input B will simultaneous update the values of state.B and state.A, fetching data from api will update values of state.B and state.C on completion, so on so forth). We need a way to manage how and when a piece of data model is mutated.Flux pattern, ReduxFlux pattern tackle this problem by providing explicitness of state mutation. Let’s take a look at a typical data flow under the flux pattern.
An example flow of data in Flux pattern
Explicitness of state mutation is achieved by the following:
Therefore, by monitoring actions and the information it contains, data mutation of the whole app is under our control (when and how something is changed).SummaryWe have went through a journey of web development tools from good old jQuery to today’s powerful ones like Angular, React, Vue. We dissect our web application into View layer and Model layer, and explore ways of managing the information flow between them. 2-way binding employs the MVVM pattern where the View is tightly bound to the Model by a ViewModel, thereby allowing each View to directly read and update its corresponding part of the Model. 1-way binding borrows the design principle from CQRS, write operation is separated from read operation, and we implement the 2 operations deliberately by ourselves. Flux pattern pushes this concept further, limiting mutation of data model to only by dispatching actions, thereby keeping the whole app state under control through close monitoring of these actions.So next time we can pause a while and think about the way to structure our apps before we start typing. Spaghetti-no-more.
Further reading:
☞ An Introduction to Test-Driven Development in JavaScript
☞ How to deploy your Vue app with Netlify
☞ The React Cookbook: Advanced Recipes to Level Up Your Next App
☞ 5 useful JavaScript array functions
☞ Firebase login functionality from scratch with React + Redux
☞ Breaking Down the Last Monolith
☞ Angular 8 - Reactive Forms Validation Example
☞ 8 Miraculous Ways to Bolster Your React Apps
#javascript