Complexity in Component-Based Frontend Architectures

19/08/2023

4 mins to read

Share article

preview

Hello, I will discuss an interesting approach I came across while researching frontend architectures, focusing on how complexity arises in large projects and how we can prevent it. By the end of the article, we will better understand the points to consider when designing frontend architectures.

Here are the fundamental questions to consider when starting:

  • What causes complexity to increase rapidly?
  • How can we achieve reusable interfaces without disrupting existing behavior?
  • How do we build maintainable and testable structures?
  • What common models can be used?

Fundamental Principles

Single Responsibility Principle: What is the single responsibility of this component?

A component should have only a single responsibility. The component should be simplified by avoiding additional tasks beyond its primary function. If a component starts doing too much, it should be broken down into smaller subcomponents.

State Management: What is the minimal definition of the state that this component holds?

The concept of state is divided into Local State and Global State. If a state can be made local to a component, it should be. The more a component is tied to global state, the less reusable it becomes. Such fundamental questions create deep differences when building a good architecture. State management is a topic in itself.

Purpose of the Component

Components are responsible for displaying and representing something on the screen. Making requests to another service is not the component's job. Such tasks should typically be handled via state management or a separate layer. This way, while the responsibilities of components remain clear and limited, the code becomes easier to maintain and develop.

Top Down vs Bottom Up

Top Down Approach

This is the most common mental model that developers tend to use. In this approach, the broad, high-level components and overall structure are determined first. Then, these main components are filled with subcomponents. In summary, it starts from the general design of the structure and moves downwards.

For example, in designing a web application, you might first determine the overall layout and main components of the page. Then, the content and details of these main components are added using subcomponents.

Bottom Up Approach

In this approach, the most basic components and functionalities are designed first. Then, these subcomponents are used to build larger and more complex components. In other words, it progresses upwards from the fundamental building blocks of the project.

For example, when creating a web application, you can first develop the basic functions and lower-level components. Then, you can combine these subcomponents to create larger and more complex components.

Top Down

Start by drawing your boxes and then fill them in. For example, consider a list of people. Let’s make it a bit more concrete. Draw the frames for the list and fill them with person cards.

preview

const contactItems = [
    { avatar: ".png", name: 'Yigit', to: '/yigit' },
    { avatar: ".png", name: 'John', to: '/john' },
    { avatar: ".png", name: 'Bill', to: '/bill' },
    { avatar: ".png", name: 'Robert', to: '/robert' },
]
...
<ContactList items={contactItems} />

The Top Down approach is simple and intuitive. Our goal is to make everything easy and reusable. We started by building the highest-level framework that we identified as necessary at the beginning.

The Top Down approach can be quite effective for small projects. However, when scaled to large codebases, this approach can lead to challenges. Let's explore how this approach can quickly escalate issues on a large scale.

Possible Future of the Top Down Approach

  1. Monolithic Components: Such components aggregate too many functions and become complex over time. This can make code management and sustainability difficult.
  2. Sunk Cost Fallacy: You might feel pressure to maintain a complex component you have previously invested in. This is known as the "sunk cost fallacy." While trying to preserve the value of old code, it can create flexibility issues when requirements change.
  3. Technology and Requirement Changes: As projects evolve, requirements and technologies may change. High-level components designed at the start may become incompatible later on.

Bottom Up

This approach is less intuitive compared to the Top Down approach and provides slower, more controlled progress initially. It encourages the creation of small, reusable components rather than large, non-reusable ones.

Creating reusable components generally allows for more readable, testable, modifiable, and removable component structures.

There is no strict rule on how small to break things down. The "Single Responsibility Principle" guides you on how small your components should be.

Example

With the Bottom Up approach, it is possible to create a large <ContactList />, but what really makes the difference is how we develop it.

You can break down the fundamental elements of the <ContactList /> component into smaller pieces and then assemble them together.

Complexity is distributed by using many small, single-responsibility components. This prevents the formation of monolithic components.

function Contact({ avatar, name, to }) {
    return (
        <div>
            <Avatar src={avatar} />
            <Name name={name} />
            <Link href={to}>{name}'s Profile</Link>
        </div>
    );
}

...

<ContactList>
 {items.map((item, index) => (
      <Contact key={index}
               avatar={item.avatar}
               name={item.name}
               to={item.to} />
  ))}
</ContactList>

The result of the Bottom Up approach is intuitive. Initially, it may require more effort due to the separation of components into smaller, self-contained pieces. However, this means adopting a more useful and sustainable approach in the long term.

Other Advantages:

  1. Different teams only pay for the small pieces they use. This way, they do not have to transfer a large component.
  2. Asynchronous operations can be coded more easily.
  3. Render performance is better and management is simpler because only the sub-component (sub-tree) needs to be recreated when an update is needed.
  4. It avoids hasty abstractions.
  5. It prevents the proliferation of monolithic components in large projects.

Conclusion

The questions you ask when designing your frontend components, and the mental models you use, influence many decisions and can drastically change the direction of the project.

Thank you :)

Y. Cakmak

© 2024, Yigit Cakmak - All rights reserved.