Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Single Page Web App UI Development Thoughts, Part 2

DZone 's Guide to

Single Page Web App UI Development Thoughts, Part 2

In Part 2 of this three-part series, we take an up close and personal look at the architectural patterns used for single page application development.

· Web Dev Zone ·
Free Resource

Welcome back! If you missed Part 1, you can check it out here.

1.3. Single Page App Architectural Pattern

1.3.1. Never Use Observers

Clarification: Observers here refers to the observer in the old Ember version (not sure if it is still there). It doesn’t contain event listeners. In older Ember versions, an observer is a function triggered when a value is changed. For example, a developer can define a function like:

function () { …}.observe(“a.value”) 

An observer is a common pattern in large web apps and many frameworks support this pattern. I have also used this pattern a lot in my previous work but I think it’s a bad pattern. People may say observers make data binding easier. It’s true if your object model is well defined and needs no more development.

Suppose value B depends on value A. Value A may be changed in many different places. If we use the observer pattern, we only need to define an observer handler on B instead of adding code to fire a change B event in every place where the value of A is changed. That’s easier. However, what if in the future you want to change value A’s updating logic? For example, suppose there are currently two places where it will change value A to the same value. Let’s say they are place C and place D. In a new task, you don’t want to change B’s value if value A is changed in place C or you may want to change B to a different value if value A is changed in place C. Then you would need to add some special flag to the context which would usually be a complicated and ugly implementation so that B’s observer could read that flag and know the change is from place C. But if you directly call change value B function from place C and D instead of using observer, you only need to add one more input parameter to the change function which is easy in Javascript.

Another example is that suppose when you were implementing B’s observer, you didn’t know every place which changed value A. And based on your imperfect knowledge of A’s modification, you decided B’s observer logic and implemented it. Then later bugs will be fired for bad behavior when some uncovered places modify A’s value. Those uncovered places may even be out of your task’s requirement. But if you don’t use observer, you only need to implement B's and A’s dependency logic based on what you know and the task requirement. For those uncovered places, it either won’t cause any bad behavior in your task related to B or it is not necessary to have the dependency. People may say observers are good where they render data directly to view because there wouldn’t be complicated logic in rendering. But this is usually supported by HTML templates in most frameworks.

So I strongly recommend using as few observers as possible.

1.3.2. Event Dispatcher - Listener Pattern Is Not Recommended

The event here is the only developer defined event. It doesn’t contain any browser events like (‘click’, ‘focus,’ etc.). A browser event listener is always fine to use. Compared to the observer pattern mentioned above, the event listener pattern is more flexible. However, I still don’t recommend using it. I think the event dispatcher pattern is mainly used in library and platform development because library/platform developers don't know how application developers want to handle events and they want to give the application developer more freedom in communication with platforms/libraries. For example, a web browser dispatches an event through JavaScript code so that the JavaScript application can handle an event at any level of the container and with any handlers.

However, within an application, there are cons:

  1. There could be many subtle UI state changes which may trigger only one other change. If we define one event for each subtle state change, there would be too many useless events to manage.
  2. If an event’s meaning isn’t clearly defined, it may be abused by a developer who is not familiar with its dispatching logic and then cause some bug in some special case.

Since web apps' UIs are usually developed by a single team, the team should completely know what logic should be triggered by a UI state change.  Therefore the main pros for the event dispatcher – listener pattern doesn’t make sense here. That’s why I don’t recommend this pattern.

1.3.3. Framework Pattern

There are many UI architectural patterns such as Model – View – Controller and Model – View – ViewModel. In each pattern, the terms “model” and “view” have slightly different meanings and responsibilities.

In the following discussion, I will use the Model – View pattern.

  • A Model is made up of classes which communicate with the backend, fetch data from the backend, and manage data model objects in the front-end.
  • A View is made up of classes which render HTML templates and handle user actions. 

As the UI grows larger, the logic and code become very complicated, so we need to apply a good design pattern to make the code highly readable, easy to maintain, and stable. The complexities are mainly in two areas:

  1. One user action could trigger a long and complicated chain of UI states and data updates.
  2. Sample chain:
  3. The dependencies of code could be very complicated, like a skein. Views could depend on each other. One view could depend on its parent, child, sibling, and even distant relatives. Data models could also depend on each other. And one view could depend on multiple data models. One model could be dependent on multiple views. Multiple views could depend on the same data model.

The above complexities could cause the following problems:

  1. Since backend calls are usually asynchronous, a long serial chain of UI state changes, backend calls, and UI renderings could exponentially increase the number of scenarios to be handled by our code.
  2. The same UI state, rendered UI, and data model could be updated multiple times in a single chain triggered by one user action.
  3. Sometimes in order to call a distant relative’s function, the developer has to use tricky or error-prone ways to obtain a reference to the distant relative object.

To address the above problems, we should:

  1. Organize and group the code into three phases: 
    Phases:
    1. Update data model with a backend API: load data from the backend. The data should contain any data that may be used after a user action.
    2. Update UI states:  JavaScript code updates all related UI states with data from the model.
    3. Render UI into HTML: Call any UI rendering callback. It shouldn’t request any new data to be loaded. All data should have been loaded in phase 1.
    To convert the serial chain in Figure 1 to Figure 2’s pattern, we should postpone and execute in advance all the same type of changes to its phases. For example, in Figure 1, some data model change happens before a UI state update and the other data model change happens after the UI state change. After conversion, we should first fetch and update all data models which could be triggered by a user action and then update all UI states together. Some data models may only need to be updated or fetched from the backend in some particular conditions, depending on the UI states. In the three-phase pattern, after a user action happens, we should predict what data is needed and always update and fetch this data. This deserves the effort and overhead because it reduces complexity.
  2. Decrease the number of dependencies among views. Use view –> model -> model -> view pattern instead. For example, instead of letting View A directly change View B’s state, let View A change model A, then Model A changes Model B, finally, Model B changes View B. The advantages of this approach are:
    1. The data model usually has a simpler structure than views. So it is easier and less tricky to find a reference of other data models and can call other data models' functions in one data model.
    2. A view can be reused without adding complicated branching logic to call other views’ functions in different conditions.

Besides the major UI architectural problems discussed above, there are two minor problems:

  1. Too many “IF” statements in common functions. Sometimes we extract some common logic into a common function. Then, as we add more and more components, we have to add a lot of special logic wrapped in “IF” blocks into the common function, which is less readable.
  2. We want to isolate a component from others so that it can be reused at different places while sometimes we also want to store other components’ references in current components.

The common solution to them is Object-Oriented Design. With OOD, we can put common logic in a basic function and extend it for a special component to add special logic. We can create a base class for a component which is most reusable. In a special application context, we can extend the base class to contain other components’ references in that context.

1.4. Debugging

Sometimes when a bug happens, it is hard to figure out the root cause because the app is very large and complicated. To make debugging easier and avoid manually adding too much console.log code, we should develop some kind of JavaScript annotation (like @debug) which tells the compiler to add a console.log statement below the next statement containing  thenext statement’s necessary information like function return, property values, and so on.

1.5. Analytics

There are two common metrics: impressions and user interaction. Sometimes impressions are hard to collect because it is hard to determine if an element’s impression happens. The impression could happen when the element is inserted into the DOM or when the element’s “display” CSS attribute is toggled or when the element is scrolled into the screen or when the element’s “z-index” is increased. So there are many scenarios that could trigger an element to be impressed.

Thus, to make it easy for analytics infrastructure developers to log the impressions, each component should implement an analytics interface which contains functions indicating if a component is impressed.

That's all for Part 2! Tune back in tomorrow when we'll wrap up this series by discussing large scale UI design and UI automation testing.

Topics:
web dev ,web application architecture ,ui development ,front-end development ,mvc architecture

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}