A different approach to frontend architecture

November 28th, 2022

frontend-architecture-thumbnail

(image source: https://www.infoq.com/i18n/software-architecture-trends-2019)

This article aims to introduce a frontend architecture (for applications built with Vue, React, Svelte, etc.) that is easy to reason about and has high maintainability. If you are building a medium/large application and often find yourself wondering about where things should be, this article might be of use to you.

The benefits of a good architecture

Before diving into any technical stuff, let's solve a small problem first:

frontend-architecture-image-1

(image source: https://pusher.com/tutorials/clean-architecture-introduction)

In the image above, can you tell me how to replace the stapler with a tape at a glance? Some of you might come up with an interesting way to do so, but for most of us, we can't immediately figure out how to solve this problem. It looks like a mess to our eyes, and it confuses our brain.

Now look at this:

frontend-architecture-image-2

(image source: https://pusher.com/tutorials/clean-architecture-introduction)

Can you now immediately tell me how to replace the stapler? We simply have to untie the string connected to it and put the tape in its place. You need a near-zero mental effort to do it.

Imagine all the items in the images above are modules or parts in your software. A good architecture should look more like the second arrangement. The benefits of such an architecture are:

  • Reducing your cognitive load/mental effort when working on the project.
  • Making your code more modular, loosely coupled, thus more testable and maintainable.
  • Easing up the process of replacing a particular part in the architecture.

The common frontend architecture

The most basic and common way to separate a frontend application nowadays can be something like this:

The common frontend architecture

There is nothing wrong with the architecture above at first. But then, a common pattern emerges from this kind of architecture where you tightly couple some parts of the architecture together. For example, this is a simple counter application written in Vue 3 with Vuex 4:

<template>
  <p>The count is {{ counterValue }}</p>
  <button @click="increment">+</button>
  <button @click="decrement">-</button>
</template>

<script lang="ts">
import { computed } from 'vue';
import { useStore } from 'vuex';

export default {
  name: 'Counter',
  setup() {
    const store = useStore();
    const count = computed<number>(() => store.getters.count);

    const increment = () => {
      store.dispatch('increment');
    };

    const decrement = () => {
      store.dispatch('decrement');
    };

    return {
      count,
      increment,
      decrement,
    };
  },
};
</script>

You will see that this is a quite common pattern in applications written with Vue 3 and Vuex because it is in Vuex 4's guide. Actually, it is also a common pattern for React with Redux or Svelte with Svelte Stores:

  • Example with React and Redux:
import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';

export const CounterComponent = () => {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();

  const increment = () => {
    dispatch({ type: 'increment' });
  };

  const decrement = () => {
    dispatch({ type: 'decrement' });
  };

  return (
    <div>
      <p>The count is {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};
  • Example with Svelte and Svelte Stores:
<script>
  import { count } from './stores.js';

  function increment() {
    count.update(n => n + 1);
  }

  function decrement() {
    count.update(n => n - 1);
  }
</script>

<p>The count is {$count}</p>
<button on:click={increment}>+</button>
<button on:click={decrement}>-</button>

There is nothing inherently wrong with these. In fact, most of the medium to large applications out there are probably written like these. They are the recommended ways in the official guides/tutorials.

However, everything is a trade-off. So, what are the advantages and the disadvantages of this pattern?

The most obvious benefit is probably simplicity.

But for that, what have you sacrificed?

You have tightly coupled the stores to the components. Now what if one day your team finds out that Redux is not the best fit for the application anymore (probably because it is overly complicated) and wants to switch to something else? Not only will you have to rewrite all your stores, you will also need to rewrite the logic of the React components that have been tightly coupled to Redux.

The same problems happen to all the other layers in your application. In the end, you cannot easily replace a part of your application with something else because everything has been tightly coupled to each other. It would just be better to leave it be and rewrite everything from scratch.

But it does not have to be that way. A truly modular architecture can allow you to replace your React + Redux application with React + MobX (or Valtio), or even crazier, React + Vuex or Vue + Redux (for whatever reason) without impacting other parts of your application.

So how do we replace a part of our application without impacting the rest, or in other words, how do we decouple every part of our application from each other?

Introducing a different approach

Introducing a different architecture The characteristics of the layers are as follows:

  • Presentation: This layer is basically made of UI components. For Vue, they are Vue SFcs. For React, they are React Components. For Svelte, they are Svelte SFCs. And so on. The Presentation Layer is directly coupled to the Application Layer.
  • Application: This layer contains application logic. It knows of the Domain Layer and the Infrastructure Layer. This layer, in this architecture, is implemented via React Hooks in React or Vue "Hooks" in Vue 3.
  • Domain: This layer is for domain/business logic. Only business logic lives in the Domain layer, so there is just pure JavaScript/TypeScript code with no frameworks/libraries whatsoever here.
  • Infrastructure: This layer is responsible for communications with the outside world (sending requests/receiving responses) and storing local data. This is an example of the libraries you would use in a real-world application for this layer:
    • HTTP Requests/Responses: Axios, Fetch API, Apollo Client, etc.
    • Store (State Management): Vuex, Redux, MobX, Valtio, etc.

Applying the architecture

If you apply this architecture to an application, it looks like this:

Applying the architecture to a React app

The following characteristics are referred from the above diagram of the architecture:

  • When you replace the UI library/framework, only the Presentation & Application layers are impacted.
  • In the Infrastructure layer, we have a Facade so that when you replace the implementation details of the store (e.g. replacing Redux with Vuex), only the store itself is impacted. The same goes for replacing Axios with Fetch API or vice versa. The Application layer does not know about the implementation details of the store or the HTTP Client. In other words, we have decoupled React from Redux/Vuex/MobX. The logic of the store is also generic enough that it can be used with not just React but also Vue or Svelte.
  • If the business logic changes, the Domain Layer will have to be modified accordingly, and that will impact the other parts in the architecture.

What is more interesting about this architecture is you can even further modularize it:

Further modularizing the architecture

Caveats

Even though the architecture can decouple the parts of your application from each other, it does come with a cost: increased complexity. Therefore, if you are working on a small application, I would not recommend using this. Don't use a sledgehammer to crack a nut.

For a more complex application, this architecture might probably help you achieve something like this:

The benefits of investing in an architecture

(image source: https://www.simform.com/react-architecture-best-practices)

An example

I have built a simple counter app that demonstrates the merits of this architecture. You can check the source code here: https://github.com/huy-ta/flexible-counter-app.

An example

In this application, I have included Vue, React and Vue with Vuex, Redux, MobX, Valtio and even localStorage. They can all be replaced without impacting each other. Follow the simple instructions from the README file and try switching a part of the application with another one.

I know that for this counter app, I'm using a sledgehammer to crack a nut, but building a complex application is a little bit out of the question for me right now.

Questions & discussions are more than welcomed 😊.

Tags
Frontend Development
share icon
Share