Front End Component Design Principles

Original article by Andrew Dinihan

Article sample code: portal

Limited by personal ability, if there is any mistake, please feel free to comment.

preface

I started developing with Vue in my most recent job, but I have more than three years of React development experience at my last company. Although switching between two different front-end frameworks does require a lot of learning, there are many basic concepts and design ideas that are common between them. One of them is component design, including the design of component hierarchy and the division of responsibilities for each component.

Components are one of the basic concepts behind most modern front-end frameworks, as seen in frameworks like React and Vue and Ember and Mithril. A component is usually a collection of markup language, logic, and style. They were created as reusable modules to build our applications.

Similar to the design of classes in traditional OOP languages, components need to be designed with many aspects in mind, so that they can be reused, combined, decoupled, and low coupling, but the functionality can be implemented stably, even beyond the scope of actual test cases. This kind of design is easier said than done, because in reality we often don’t have enough time to do it optimally.

methods

In this article, I want to introduce some component-related design concepts that should be considered when doing front-end development. I think the best approach is to give each concept a succinct and concise name, and then explain what each concept is and why it’s important, with some examples for more abstract concepts.

The following list is by no means exhaustive or complete, but I’ve noticed only eight things worth mentioning for people who can already write basic components but want to improve their technical design skills. So here’s the list: The list below is just eight of the aspects I noticed, as well as a few other aspects of component design. Here I just list what I think is worth mentioning.

For developers who have mastered the basics of component design and want to improve their component design skills, here are eight things I think are worth noting, but not all of them.

  1. Hierarchy and UML class diagrams
  2. Flat, data-oriented state/props
  3. A more pure change in State
  4. Low coupling
  5. Auxiliary code separation
  6. extract
  7. Just-in-time modularization
  8. Centralized/unified state management

Note that the code examples may have some glitches or be a little contrived. But they’re not complicated, just examples to help you understand the concepts.

Hierarchy and class diagrams

Together, the components within your application form a component tree, and visualizing the component tree during the design process can help you understand the overall layout of your application. A good way to show this is through component diagrams.

There is a type in UML that is often used in OOP class design called UML class diagrams. Class diagrams show class attributes, methods, access modifiers, relationships between classes and other classes, and so on. Although OOP class design and front-end component design are very different, the method of graphically aided design is worth referring to. For front-end components, the diagram can show:

  • State
  • Props
  • Methods
  • Relationship to Other Components

So, let’s take a look at the following component hierarchy diagram of the base table component, whose render object is an array. The capabilities of this component include displaying the total number of rows, the header row, and some data rows, and sorting the column when its cell header is clicked. In its props, it passes a list of columns (with a property name and a human-readable version of that property), and then passes an array of data. We can add an optional ‘on Row click’ function to test this.

While this kind of thing may seem like a lot, it has many advantages and is needed in large application development designs. An important issue with this is that it requires you to think about the implementation details before you start codeing, such as what type of data each component needs, what methods to implement, what state attributes are required, and so on.

Once you on how to build a component (or a set of components) about the whole train of thought, would be easy to think that when you really start coding, it will be done as we expected step-by-step, but in fact will often appear some unexpected things, of course you don’t want so before going to refactor some parts, Or put up with flaws in the original idea and mess with your code. The following advantages of these class diagrams can help you avoid the above problems. The advantages are as follows:

  1. An easy-to-understand view of component composition and association
  2. An easy-to-understand overview of the application UI hierarchy
  3. A view of the hierarchy of structured data and how it flows
  4. A snapshot of a component’s functional responsibilities
  5. Easy to create using charting software

By the way, the diagram above is not based on some official standard like UML class diagrams, it’s a set of presentation rules that I basically created. For example, data type definition declarations for props, method parameters, and return values are based on Typescript syntax. I haven’t found an official standard for writing front-end component class diagrams, probably due to the relatively new and incomplete ecosystem of front-end Javascript development, but if anyone knows of a mainstream standard, please let me know in the comments!

Flat, data-oriented state/props

If you use nested data with frequent watches and updates for state and props, your performance may suffer, especially in scenarios such as rerenders triggered by shallow alignment; In libraries that involve immutability, such as React, you have to create copies of states rather than change them directly as in Vue, and doing so with nested data can create awkward, ugly code.

Even with the expansion operator, this is not elegant. Flat props also does a good job of cleaning up data values that the component is using. If you’re passing an object to a component and you don’t know exactly what the property values are inside the object, it’s extra work to find out if the data values you actually need come from the component-specific property values. But if the props were flat enough, it would at least be easy to use and maintain.

State/props should also contain only the data needed for component rendering. You shouldn’t store entire components in the state/props and render straight from there.

(Also, for data-heavy applications, data normalization can bring huge benefits, and you may want to consider other optimizations besides flattening).

A more pure change in State

Changes to state should usually respond to some kind of event, such as a user clicking a button or an API. Furthermore, they should not respond to changes in other states, because such associations between states can lead to component behavior that is difficult to understand and maintain. State changes should have no side effects.

If you abuse watch instead of limiting your consideration of the above principles, then this can cause problems in your use of Vue. Let’s look at a basic Vue example. I am studying a component that obtains some data from API and presents it to the table. Sorting, filtering and other functions are completed by the back end, so all the front end needs to do is watch all the search parameters and trigger API calls when they change. One of the values required for watch is “zone”, which is a filter. When changing, we want to retrieve the server data using the filtered value. Watcher is as follows:

You’ll find some strange things. If they go beyond the first page of results, we reset the page number and end, okay? This doesn’t seem right, if they’re not on the first page, we should reset the page and trigger the API call, right? Why do we only retrieve data on page 1? Actually, here’s why. Let’s take a look at the watch in its entirety:

When a page changes, the application first retrieves the data through pagination’s handler. Therefore, if we change pagination, we don’t need to worry about the data update logic.

Let’s consider the following flow: If the current page goes beyond page 1 and the zone changes, that change triggers another state change, which triggers the observer of Pagination to request data again. This is not an expected behavior, and the resulting code is not intuitive.

The solution is that the event handler that changes the page number (not the observer, the actual handler that the user changes the page) should change the page value and trigger an API call to request the data. This will also eliminate the need for observers. With this setup, changing paging status directly from elsewhere does not cause the side effect of retrieving data.

While this example is very simple, it’s not hard to see how correlating more complex state changes can produce incomprehensible code that is not only unextensible but also a debugging nightmare.

Loose coupling

The core idea of components is that they are reusable, and for that they must be functional and complete. “Coupled” is the term for entities that depend on each other. Loosely coupled entities should be able to operate independently of other modules. In terms of front-end components, the main part of coupling is how much a component’s functionality depends on its parent and the props it passes, as well as the child components it uses internally (and of course the referenced parts, such as third-party modules or user scripts).

Tightly coupled components are less likely to be reused, making it harder to work as children of a particular parent, and making code redundant when a child or series of children of a parent function only as well as the parent. Because the parent and child components are not overly correlated.

When designing components, you should consider more general usage scenarios, not just to meet the needs of a particular scenario from the beginning. It doesn’t matter that components are originally designed for a specific purpose, but many components would be more applicable if they were designed from a higher perspective.

Let’s look at a simple React example where you want to write out a list of links with a logo that access a specific website. The initial design may not be properly decoupled from the content. Here’s the original version:

While this will satisfy the intended usage scenarios, it is difficult to reuse. What if you want to change the link address? You must copy the same code again and manually replace the link address. Also, if you want to implement a feature where the user can change the connection, which means that it is impossible to write the code “dead” and expect the user to manually change the code, let’s look at the design of a more reusable component:

Here we can see that although its original link and logo have default values, we can override the default values passed in by props. Let’s see it in action:

No need to rewrite new components! If we address the above scenario where users can customize links, we can consider dynamically building link arrays. Also, although not addressed in this specific example, we can still notice that this component is not closely associated with any particular parent/child. It can be presented wherever it is needed. The improved component is significantly more reusable than the original version.

Instead of designing a component that needs to serve a particular one-off scenario, the ultimate goal of designing a component is to have it loosely coupled to its parent for better reuse, rather than being constrained by a particular context.

Auxiliary code separation

This may be a less partial theory, but I still think it’s important. Working with your code base is part of software engineering, and sometimes some basic organizational principles can make things smoother. Changing even a small habit can make a big difference over a long period of time with code. One of the principles that works is to separate out the auxiliary code and put it in a specific place so that you don’t have to think about it when you’re working with components. Here are some:

  • Configuration code
  • False data
  • Lots of non-technical documentation

Because when you’re trying to work with the core code of a component, you don’t want to see instructions that aren’t technically relevant (because you’ll scroll the mouse wheel a few times or even interrupt your train of thought). When working with components, you want them to be as generic and reusable as possible. Looking at specific information related to the current context of a component can make it difficult to decouple a designed component from a specific business.

extract

While this can be challenging, a good way to develop components is to have them include the minimal Javascript required to render them. Things that don’t matter, such as data retrieval, data collation, or event handling logic, should ideally be moved to an external JS or placed in a common ancestor.

Viewed separately from the “view” section of the component, that is, what you see (HTML and styles). The Javascript is only there to help render the view, and there may be some component-specific logic (for example, when used elsewhere). Anything else, such as API calls, formatting of values (such as currency or time), or reusing data across components, can be moved outside the JS file. Let’s take a look at a simple example in Vue, using nested list components. We can start with the problematic version below.

This is the first level:

Here is the nested list component:

Here we can see that both levels of this list have external dependencies, with the top layer directing data from functions and JSON files in external JS files, and nested components connected to the Vuex store and sending requests using AXIOS. They also have embedding capabilities that are only applicable to the current scenario (specific response capabilities for mid-click time source data processing and nested lists in the top layer).

While there are some good general-purpose design techniques, such as moving common data processing to external scripts rather than just writing functions to death, this is still not very reusable. What if we get data from an API response, but the data is of a different structure or type than we expected? Or do we expect different behavior when we click on nested items? In the case of these requirements, the component cannot be directly referenced by other components and can change its features based on actual requirements.

Let’s see if we can solve this problem by elevating the data and passing the event handling as props, so that the component can simply render the data without encapsulating any other logic.

This is the first level of improvement:

And the new second level:

With this new list, we can get the data we want and define a nested list of onClick handlers to pass in any operations we want in the parent level and then pass them as props to the top-level component. This way, we can leave the import and logic to a single root component, so there is no need to re-implement a similar component for use in a new scenario.

A short article on the subject can be found here. It was written by Redux author Dan Abramov, although it uses React as an example. But the idea of component design is universal.

Just-in-time modularization

We carried out in actual components out of work, don’t need to be taken into account too much componentized, admittedly chunk of code into loose coupling and the use of part is good practice, but not all of the pages (HTML) structure need to be pulled into components, not all the logical part needs to be pulled out to the outside of the component.

When deciding whether to separate your code, whether it’s Javascript logic or pulling it out to new components, consider the following points. Again, this list isn’t complete, just to give you an idea of the various things to consider. (Remember, just because it doesn’t meet one condition doesn’t mean it won’t meet the others, so consider all conditions before making a decision) :

  1. Is there enough page structure/logic to warrant it? If it’s just a few lines of code, you might end up creating more code to separate it than just putting code in it.
  2. Code duplication (or possible duplication)? If something is only used once and serves a specific use case that is unlikely to be used elsewhere, it might be better to embed it. Feel free to split them up if you need to (but don’t use this as an excuse to slack off when you need to do the work).
  3. Will it reduce the number of templates to write on? For example, suppose you want a component with a div attribute structure with a certain style and some static content/functionality, with some variable content nested inside. By creating reusable wrappers (like React HOC or Vue Slot), you can reduce template code when creating multiple instances of these components because you don’t have to rewrite external wrappers.
  4. Will performance be affected? Changing the state/props will cause re-rendering, and when that happens, all you need to do is re-render the related element nodes that were diff. In larger, closely related components, you may find that a state change causes rerendering in many places where it is not needed, and application performance may begin to suffer.
  5. Do you have problems testing all parts of your code? We always want adequate testing, such as for a component, where we expect it to work independently of a particular use case (context), and all Javascript logic to work as expected. When the element has a particular assumed context, or when a bunch of logic is individually embedded into a single function, it can be difficult to meet our expectations. Rendering tests of components can also be difficult if the components being tested are single giant components with large templates and styles.
  6. Do you have a specific reason? When splitting code, you should consider what it actually implements. Does this allow for looser coupling? Am I breaking a separate entity that logically makes sense? Is it really possible for this code to be reused elsewhere? If you can’t answer this question clearly, it’s best not to do component extraction yet. This can lead to problems (such as uncoupling some of the underlying coupling).
  7. Do the benefits outweigh the costs? Separating code inevitably takes time and effort, the amount of which varies depending on the situation, and there are many factors (such as the ones listed in this list) that ultimately lead to this decision. In general, doing some research on the costs and benefits of abstractions can help make faster and more accurate decisions about whether or not componentization is needed. Finally, I mentioned this, because if we focus too much on the advantages, it’s easy to forget how much effort is needed to achieve our goals, so you need to weigh both before making a decision.

Centralized/unified state management

Many large applications use state management tools like Redux or Vuex (or have state-sharing Settings like the Context API in React). This means that they get props from the store rather than passing it through the parent. When thinking about component reusability, you need to think not only about props passed in from the immediate parent, but also props obtained from the Store. If you use this component in another project, you need to use these values in the store. Maybe other projects don’t use the centralized storage facility at all, and you have to convert it to props passed from the parent.

Because it is easy to hook a component into a Store (or context) and can be done regardless of the component’s hierarchical location, it is easy to quickly create a lot of tight coupling between the store and the components of a Web application (regardless of the component’s hierarchy). Associating a component with a Store is usually just a few lines of code. Note, however, that while this connection (coupling) is more convenient, it doesn’t mean anything different, and you need to consider trying to conform to the same points as when using parent passes.

The last

I would like to remind you that you should pay more attention to the above component design principles and the best practices you already know applied in practice. While you should do your best to maintain good design, don’t compromise code integrity by wrapping up a JIRA ticket or a cancellation request, and people who always put theory ahead of real-world results tend to let their work suffer. Large software projects have many moving parts, and there are many aspects of software engineering that are not specifically related to coding, but are nonetheless indispensable, such as meeting deadlines and dealing with non-technical expectations.

While thorough preparation is important and should be part of any professional software design, in the real world it is the practical results that matter most. When you’re hired to actually create something, your employer probably won’t be too happy if all you have before the deadline is an amazing plan for how to build the perfect product, but no actual results? In addition, things in software engineering rarely go exactly as planned, so overly specific planning often backfires in terms of time use.

In addition, the concepts of component planning and design also apply to component refactoring. While it would have been nice to spend 50 years planning everything in excruciating detail and then write it perfectly from the start, back in the real world, we tend to run into situations where we can’t make code perfect as expected in order to catch up. However, once we have free time, the recommended course of action is to go back and refactor the earlier less-than-ideal code so that it can serve as a solid foundation to move forward.

At the end of the day, while your immediate responsibility may be to “write code,” you shouldn’t lose sight of your ultimate goal, which is to build something. Create the product. Always remember to find a balance in order to produce something you can be proud of and help others, even if it’s not technically perfect. Unfortunately, staring at the code in front of you for 8 hours a day for a week can lead to a narrower perspective, when you need to step back and make sure you don’t lose the forest for one tree.