DEV Community

loading...
Cover image for A different approach to frontend architecture

A different approach to frontend architecture

quochuytlbk profile image Huy Ta Quoc ・6 min read

(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:

(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:

(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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

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, 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. However, at Linagora, we are trying to apply this architecture to one of our projects - Twake Console, which is going to be open-sourced soon. Please look forward to it.

Questions & discussions are more than welcomed 😊.

Discussion (18)

pic
Editor guide
Collapse
jackmellis profile image
Jack

👍 really good article. I've been writing FE apps almost identical to this for a few years now and it's a great pattern to use. Bonus points if you use dependency injection to further decouple your layers!

Collapse
quochuytlbk profile image
Huy Ta Quoc Author • Edited

Hi Jack, thanks for reading the article 😃.

In the example app, I'm already doing some simple manual dependency injections in the Application layer. In the frontend world, we still have to keep in mind code splitting & lazy loading, so dependency injection is a little bit different from what we do in the backend side I think.

Collapse
jackmellis profile image
Jack

Yes code splitting is a big issue for DI on the front end. In a node app, for example, you can just grep all of your dependencies when bootstrapping with very little cost. This is actually something I'm trying to work out right now 🤔

Thread Thread
quochuytlbk profile image
Huy Ta Quoc Author

@jackmellis Please write an article about it if you ever figure it out 😍.

Collapse
tuanlc profile image
Lê Công Tuấn

Bonus points if you use dependency injection to further decouple your layers!

How do you use dependency injection on the FE side?

Collapse
quochuytlbk profile image
Collapse
leob profile image
leob • Edited

Great article!

This is known as Hexagonal Architecture, right? DDD (Domain Driven Design) is another closely related concept. Problem is that most devs aren't familiar with it, and that for most small to medium apps it isn't really worth it. But for larger apps I'd say yes, it's worth looking at.

P.S. what I like about this approach is that you raise the abstraction level, and that you focus on the problem rather than the implementation, and you cut yourself loose from endless discussions about Redux, versus Context, versus whatever. What I like less about it is the added boilerplate, if you can get rid of that then you have a win win situation.

Collapse
quochuytlbk profile image
Huy Ta Quoc Author • Edited

I don't know what this architecture should be called. Some say it's like Hexagonal Architecture, some say it's related to DDD. All I know is it got inspired by a lot of architectures 😁.

And yes, the trade-off of this architecture is it will add more boilerplates. So in the end it might not be worth it if you want to quickly build a small application.

Collapse
michaelbailly profile image
MichaelBailly

Hello Huy, thank you for this awesome article ! It reminds me a lot Domain Driven Design, applied to frontend. Well done !

Collapse
quochuytlbk profile image
Huy Ta Quoc Author

Thanks for dropping by. This architecture is indeed inspired by the CLEAN architecture, the Onion architecture and DDD, with a few tweaks to make it applicable to the frontend world.

Collapse
talyssonoc profile image
Talysson de Oliveira

What do you think of adding this to the post? Might be useful for people that are new to these concepts to understand the ideas didn't come outta nowhere. We lose a lot to context when we don't reference the canonical source of this kind of info.

Collapse
cfuehrmann profile image
Carsten Führmann

Very nice article! I'm a full stack developer used to Clean Architecture on the backend. Obviously, the frontend world has a lot of catching up to do in the architecture department. One specific thing that strikes me how is frontend state management (like Redux) should be passed into presentational components by dependency injection instead of the way it's currently done. It has to be said though that DI seems harder to do nicely in the JavaScript/TypeScript world. One reason why DI nicer in the backend is that backend languages allow DI containers to use injectees' static types (since those types are around even at runtime and can be accessed via reflection, unlike in TypeScript). So it seems a DI container in JavaScript or TypeScript will necessarily have a less convenient API,

Collapse
quochuytlbk profile image
Huy Ta Quoc Author

One specific thing that strikes me how is frontend state management (like Redux) should be passed into presentational components by dependency injection instead of the way it's currently done.

I totally agree. I don't really like the idea of a state management tool being tightly coupled to UI components, but that is probably what library authors want though.

Collapse
remshams profile image
Mathias Remshardt • Edited

Thank you for the concise article/summary.

We almost do/did everything like that but the facade for the store.
I always thought exchanging the store would never be required/an option. Turned out in one of our projects we had to do exactly that (replacing redux with plain angular services).
Even though we had no facades for the store in place, the separation of the domain and presenters into dedicated services/components already helps a lot. A huge part of the (old) redux based implementations is now replaced and it is/was possible without too much effort/pain.
So even if it seems like a lot of effort in the beginning these abstractions help a lot in the long run.

Collapse
quochuytlbk profile image
Huy Ta Quoc Author

Yes, the benefits of this architecture will really shine the most in the long run even though you might find it a little bit verbose at first.

Collapse
tuanlc profile image
Lê Công Tuấn

Nice article!

Collapse
fabiocosta89 profile image
Fabio Costa

I like this architecture. It is simple and well divided.

Thanks for sharing.

Collapse
quochuytlbk profile image
Huy Ta Quoc Author

Glad to hear you like it!