Since I started working on frontend projects, I've been looking for clean architectures that align with some of my basic development principles: maintainable, scalable, and readable code. After a long search, I've come to the conclusion that the architecture depends on the project, not the technologies we use. Therefore, although we're applying this model to React and Redux, it can be used with many other web frameworks or libraries.
The ultimate goal is to have a general picture of how to model our frontend projects with React and your preferred state manager. We'll cover folder structure, hexagonal architecture, component management, CSS architecture, and of course, testing.
The development of clean architectures in the frontend has been organic. We come from a time when all business logic was in the backend. The web was nothing more than static pages rendered by a server with little business logic. Then we moved on to jQuery, which gave us a bit more autonomy, such as validations or UI logic, until we reached the current technologies: SPAs, which allow us to build real applications executed in the user's browser.
From the first to the last, only a few years have passed, and with such rapid growth, few companies have stopped to consider whether it really makes sense to start standardizing the code written in the front-end. After all, we're just rendering things on the DOM, right?
Clean architectures have a purpose beyond (usually) increasing the number of files in our project: decoupling the business logic from the rest of the application elements. In this case, decoupling it from both the backend connections and the interface. Let's imagine the case of a Trello-style application. It's independent of how the component to create a task is displayed, the logic that will be executed when clicking the "Save" button will be the same. Now imagine if all the save logic is on that component. It would be hard to find, and refactoring that component would be like disarming a bomb.
Now imagine that when clicking on the save button, a use case is invoked with the parameters that the user has introduce. This use case performs the validations and creates a new object with all the data, which it sends to the repository responsible for persisting it. The repository, decoupled from the rest of the logic, is currently saving the information in local storage because we want to create an MVP to see if the project is reliable, but in the future we'll want to connect it to an API. This repository also updates the state in Redux so that the change is reflected in the UI.
In this brief case, we have used several patterns and best practices such as the repository pattern or the hexagonal architecture. We will delve into each of these topics in more detail in the upcoming chapters of the series. But let's take a closer look at what hexagonal architecture is all about.
The hexagonal architecture (or ports and adapters architecture) is a model designed to decouple the domain logic, application logic, and infrastructure.
As many of you may already know how this works, I will be as brief as possible. However, if you are interested in a more detailed article, I can write one that focuses more on the frontend
This architecture should be viewed as an onion with three layers (from the outside in): infrastructure, application, and domain. Where infrastructure knows about application and domain, application knows about domain, and domain only knows about itself.
We come to the million-dollar question: Where does React fit in the hexagonal architecture? To answer this complicated question, we have a simple answer: in the infrastructure layer. As explained in its documentation, React is a library for building user interfaces, nothing more. Therefore, we will fit it there, just like other libraries or frameworks such as Vue or Svelte. So the previous case is structured as follows:
React, JSX components, hooks, interface logic, styles, and Redux are all located in the infrastructure layer. The repository implementation and Local Storage client (which will be replaced with a REST client in the future) are also located here.
Use cases such as creating a new task, assigning it to a user, adding a note, or deleting a task are located in the application layer.
Finally, in the domain layer, we have domain element types, repository interfaces, and domain logic in general.
Sounds better, doesn't it? Now we have everything in its place. It's easier to find, but for that, we need to have a good folder structure, which is what we'll cover in the next chapter of the series.