DEV Community

Rubem Vasconcelos
Rubem Vasconcelos

Posted on

Clean Architecture: Applying with React

This text is part of a series of texts about Clean Architecture analysis applied with different frameworks and languages.

The purposes of this text are in line with those of the previous text, which are: I. Show an architectural division of a React application using Clean Architecture; II. Guide the implementation of new features in this proposed architecture.

Architectural Division

The initial step is to analyze how the division is performed.

cypress/
src/
  data/
    protocols/
    test/
    usecases/
  domain/
    errors/
    models/
    test/
    usecases/
  infra/
    cache/
    http/
    test/
  main/
    adapters/
    config/
    decorators/
    factories/
      cache/
      decorators/
      http/
      pages/
      usecases/
    routes/
    scripts/
    index.tsx
  presentation/
    assets/
    components/
    hooks/
    pages/
    protocols/
    routes/
    styles/
    test/
  requirements/
  validation/
    errors/
    protocols/
    test/
    validators/
Enter fullscreen mode Exit fullscreen mode

In detail, the purpose of each file structure is the following:

  • cypress: It contains the application's end-to-end test files (for large projects, this folder is recommended to be in a separate project, so that the team responsible for e2e tests can take care of it, as they do not need to know the project code).
  • src: Contains all files needed for the application.
    • Data: The data folder represents the data layer of the Clean Architecture, being dependent on the domain layer. Contains the implementations of business rules that are declared in domain.
    • Domain: Represents the domain layer of the Clean Architecture, the innermost layer of the application, not having any dependency on any other layer, where it contains the business rules.
    • Infra: This folder contains the implementations referring to the HTTP protocol and the cache, it is also the only place where you will have access to external dependencies related to these two items mentioned. This folder also contains most of the external libraries.
    • Main: It corresponds to the main layer of the application, where the interfaces developed in the presentation layer are integrated with the business rules created in the folders that represent the innermost layers of the Clean Architecture. All this is due to the use of design patterns such as Factory Method, Composite and Builder.
    • Presentation: This folder contains the visual part of the application, with its pages, components, hooks, assets and styling.
  • Requirements: Contains documented system requirements.
  • Validation: Where it contains the implementations of the validations used in the fields.

Unlike the approach with Flutter - where there was a central folder where all the tests were concentrated - in this approach the tests are located in the respective folders inside the src.

Implementation Guide

In this section, a recommended logical sequence will be described for a better implementation performance of React systems using this architecture.

In order to simplify the explanation, unit tests will not be described in detail. However, it is strongly recommended to start with unit tests before development (TDD) of each step using the requirements to support the scenarios. And after finalizing the scenarios, test the end to end flow (if it is one of the main ones, keep in mind the test pyramid).

The following demonstration is the creation of the Login flow to log into an application.

First step: Create business rules in the domain layer

Inside src/domain/usecases, create authentication.ts. This file will be an interface that will describe the authentication business rule.

import { AccountModel } from '@/domain/models/';

export interface IAuthentication {
  auth(params: Authentication.Params): Promise<Authentication.Model>;
}

export namespace Authentication {
  export type Params = {
    email: string;
    password: string;
  };

  export type Model = AccountModel;
}
Enter fullscreen mode Exit fullscreen mode

As we can see, it is an interface that has an auth() function that receives the Authentication.Params which are declared in a namespace below - containing the parameters type (email and password) and the model type (AccountModel) - and expects to return an Authentication.Model asynchronously.

AccountModel is a named export of the model created in src/domain/models that represents the token that is returned after authentication to persist the session.

export type AccountModel = {
  accessToken: string;
};
Enter fullscreen mode Exit fullscreen mode

Second step: Implement the rules in the data layer

In this layer, we create the use case to implement the rule created previously in the domain layer, but inside src/data/usecases.

The file usually looks like the example below.

import { IHttpClient, HttpStatusCode } from '@/data/protocols/http';
import { UnexpectedError, InvalidCredentialsError } from '@/domain/errors';
import { IAuthentication, Authentication } from '@/domain/usecases';

export class RemoteAuthentication implements IAuthentication {
  constructor(
    private readonly url: string,
    private readonly httpClient: IHttpClient<RemoteAuthenticationamespace.Model>
  ) {}

  async auth(
    params: Authentication.Params
  ): Promise<RemoteAuthenticationamespace.Model> {
    const httpResponse = await this.httpClient.request({
      url: this.url,
      method: 'post',
      body: params,
    });

    switch (httpResponse.statusCode) {
      case HttpStatusCode.ok:
        return httpResponse.body;
      case HttpStatusCode.unauthorized:
        throw new InvalidCredentialsError();
      default:
        throw new UnexpectedError();
    }
  }
}

export namespace RemoteAuthenticationamespace {
  export type Model = Authentication.Model;
}
Enter fullscreen mode Exit fullscreen mode

As we can see, the RemoteAuthentication class implements the IAuthentication interface, receiving the HTTP client and the url for the request. In the auth() function it receives the parameters, and calls the httpClient passing the url, the method (in this case it is post) and the body (which are the parameters). This return is a httpResponse of the type referring to the Authentication.Model that has a response status code, and which, depending on its result, gives the respective return - and may return the value expected by the request or an error.

The status codes are the HTTP:

export enum HttpStatusCode {
  ok = 200,
  created = 201,
  noContent = 204,
  badRequest = 400,
  unauthorized = 401,
  forbidden = 403,
  notFound = 404,
  serverError = 500,
}
Enter fullscreen mode Exit fullscreen mode

Third step: Implement the pages in the presentation layer

To simplify the understanding, only code snippets referring to the authentication function call will be presented. The Login screen contains more actions and details that go beyond authentication. Consider the page prototype below for easier visualization.

Login page

In src/presentation/pages/ the Login page will be created, which is composed by components, methods and functions. The component that calls the authentication function is the <Button/> that is contained in the form to get the input values, as shown in the following code snippet:

<form
  data-testid="loginForm"
  className={Styles.form}
  onSubmit={handleSubmit}
> 
  <Input
    autoComplete="off"
    title="Enter your e-mail"
    type="email"
    name="email"
  />
  <Input
    autoComplete="off"
    title="Enter your password"
    type="password"
    name="password"
    minLength={6}
  />
  <Button
    className={Styles.loginBtn}
    type="submit"
    disabled={state.isFormInvalid}
    title="Login"
    data-testid="loginButton"
  />
</form>
Enter fullscreen mode Exit fullscreen mode

When clicking on the Button, the handleSubmit() that is in the onSubmit of the form is called.

const handleSubmit = async (
    event: React.FormEvent<HTMLFormElement>
  ): Promise<void> => {
    event.preventDefault();
    try {
      const account = await authentication.auth({
        email: state.email,
        password: state.password,
      });

      setCurrentAccount(account);
      history.replace('/');
    } catch (error) {
      // Error handling here
    }
  };
Enter fullscreen mode Exit fullscreen mode

Where the authentication.auth() on click will call a factory (we'll see later) to do the authentication. In this case, it is passing the parameters captured by the input and the value returned from the request is saved in the cache through setCurrentAccount(account).

Fourth step: Connect all layers for requests to work

After everything is implemented, now just connect all the parts. For this, the design pattern Factory Method is used.

Inside src/main/factories/usecases we create the factory of the use case being implemented. In the case of this example, it is related to authentication.

The makeRemoteAuthentication is created, which returns the RemoteAuthentication that receives as a parameter the factory of the Http Client and the factory that creates the URL. The URL of the API you want to request is passed as a parameter along with the factory that creates the URL. In the example it is the URL that ends with /login.

import { RemoteAuthentication } from '@/data/usecases/';
import { IAuthentication } from '@/domain/usecases';
import { makeAxiosHttpClient, makeApiUrl } from '@/main/factories/http';

export const makeRemoteAuthentication = (): IAuthentication => {
  const remoteAuthentication = new RemoteAuthentication(
    makeApiUrl('/login'),
    makeAxiosHttpClient()
  );

  return remoteAuthentication;
};
Enter fullscreen mode Exit fullscreen mode

After that, in src/main/factories/pages, the folder for the Login factories is created. In pages with forms, form validations are also injected, but as the focus of this text is on integrations, we will leave this point out of the explanation.

import React from 'react';
import { Login } from '@/presentation/pages';
import { makeRemoteAuthentication } from '@/main/factories/usecases/';

const makeLogin: React.FC = () => {
  const remoteAuthentication = makeRemoteAuthentication();

  return (
    <Login
      authentication={remoteAuthentication}
    />
  );
};

export default makeLogin;
Enter fullscreen mode Exit fullscreen mode

A makeLogin const representing the factory is created. It has makeRemoteAuthentication which is injected inside the Login page created in the presentation layer so that the page has access to these requests.

Fifth step: Apply the page created in the application

Finally, it is necessary to call the Login factory in the application, so that it can be accessed by the user.

In the router.tsx file located in src/main/routes, add the factory page created into the Switch inside BrowserRouter. The route is passed in the path, in this case it is /login, and the page in the component, which in this case is the pointer to the makeLoginPage factory . This logic is used with all other pages, only changing from Route to PrivateRoute if the route is authenticated. The code looks like this below.

const Router: React.FC = () => {
  return (
    <ApiContext.Provider
      value={{
        setCurrentAccount: setCurrentAccountAdapter,
        getCurrentAccount: getCurrentAccountAdapter,
      }}
    >
      <BrowserRouter>
        <Switch>
          <Route exact path="/login" component={makeLogin} />
          <PrivateRoute exact path="/" component={makeDashboard} />
        </Switch>
      </BrowserRouter>
    </ApiContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Clean Architecture despite being a bit complex to understand and implement at the beginning - and even seem redundant -, abstractions are necessary. Several design patterns are applied to ensure the quality and independence of the code, facilitating the evolution and independent maintenance of the framework. In cases like this, if you want to change the framework from React to Angular or any other Typescript based framework, just change the presentation layer and make adjustments to the dependencies.

Following the development process and understanding why you are doing it in such a way makes code production easier. After a while it ends up being done naturally, as it has a linear development process: I. Use case in the domain layer; II. Use case in the data layer; III. Creation of UI in the presentation layer; IV. Creation of factories to integrate all layers into the main layer; V. And the call of the main factory in the application routes.

As the example has many abstracted parts, it is recommended that you read the code for the hidden parts for a better understanding. In this repository you can access abstracted code similar to the one given in this example.

You can also access this architecture just by running the npx @rubemfsv/clean-react-app my app command, similar to create-react-app, but in a cleaner and more scalable way. Find out how to do it reading this post.

References

  • Rodrigo Manguinho https://github.com/rmanguinho/clean-react
  • MARTIN, Robert C. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. 1st. ed. USA: Prentice Hall Press, 2017. ISBN 0134494164.

Discussion (24)

Collapse
chasm profile image
Charles F. Munat

I don't find this "clean" at all. I find it remarkably annoying. And it's everywhere.

When I work on an app, I don't generally work on all the components, or all the hooks, or all the routes. I work on FEATURES.

The best way to organize an app, especially a front end app, is by FEATURE. If I have, for example, a profile page, then I have a Profile folder and I have everything to do with that feature in that folder: routes, api calls, tests, storybook, CSS, components that are unique to that FEATURE, etc.

If I have shared components, then I put them in a shared folder no higher than the node at which the branches that use that component meet. Ditto for utility functions.

This way, I have everything right where I need it when I'm working on a FEATURE. Isn't that what most devs work on? My application's folder hierarchy follows the nesting of the components in the app. If a component is used only by another component, then it is in its parent component's folder: right where you'd expect it to be.

And if the feature needs to be moved, I just move the folder and change the references (which VSCode does for me automatically). If I delete it, I delete everything and I only have to find the references to it (e.g., a route in a router). I don't have to search through dozens of folders and files to find all the pieces.

I explained this as part of a discussion on micro-apps here: Micro-app file/folder hierarchy.

If only more devs would try this, I think they'd like it. Think about how the code ought to be organized (loosely-coupled features) rather than some arbitrary taxonomy of file types.

I suspect that the system recommended in this article, which is, frankly, ubiquitous, is the result of too many back end devs trying to do front end architecture and just using what they know. The client side deserves better.

Collapse
tiagoha profile image
Tiago A • Edited on

I agree 10000% with you.
The way you said it is a much more practical way of organizing projects on the frontend.
The clean arc works great on the backend, but it stops there.
While it's good for a frontend to be inspired by backend architectures, it needs to be clear about what works and what doesn't and needs adaptation.

Collapse
damian_cyrus profile image
Damian Cyrus

Not forgetting how much easier it get when you start to use feature flags.

But I don't want to criticise the way others work, as long as it delivers a good, maintainable product.

Collapse
bwca profile image
Volodymyr Yepishev

It would be actually interesting to see this clean architecture shown in a monorepo where the clean parts reside in a library and are re-used by two different frameworks, i.e. create-react-app and Angular.

Collapse
jackmellis profile image
Jack

This is something I'm always keen on. I hate it when libraries for what could be framework-agnostic tools are tightly coupled to the framework.
"If you want to fetch the price of milk install this library and call useMilkPrice()"
What if I want to fetch the price of milk outside of a react context.
This has also been a big annoyance when moving between frameworks. A library that could easily be applied to a vue application has to be abandoned because it's tightly coupled to react.
I always prefer a core library and then a framework-specific wrapper or adapter library.
Rant over.

Collapse
rubemfsv profile image
Rubem Vasconcelos Author

Very interesting point! It's already in my plans. hehe
Thank you for the contribution.

Collapse
delacernamarimar profile image
Marimar Dela Cerna

I agree and waiting.

Collapse
peerreynders profile image
peerreynders

Lately CUPID has been making the rounds (TwTR; largely pushing back against the dogma associated with Clean X and SOLID).

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
jackmellis profile image
Jack

Yikes. Good architecture and having design principles in place makes coding faster, eases maintenance, and makes your code more robust. Hell, application structure is about 50% of my job.
And by "new age of developers" I assume you're aware the author of the cupid article was born in the 60s 🤔

Collapse
elsyng profile image
Ellis

A little harsh, tho true :o)

Collapse
jackmellis profile image
Jack

I've not come across cupid before but I love it!
One thing that always irks me about solid is it's so steeped in OOP, you really have to twist and squint to apply it to anything else. (It's ironic that principles around coupling are tightly coupled.)
Cupid is actually much easier to apply to FP because it's more about attitudes and approaches than language-specific constructs.
Thanks for sharing!

Collapse
silverium profile image
Soldeplata Saketos

Now you can update to React 18 and stop using classes. Function Components! :)

Collapse
rubemfsv profile image
Rubem Vasconcelos Author

Thanks for the contribution!
Hope to do that next haha It's an ongoing project.

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
rubemfsv profile image
Rubem Vasconcelos Author

To be honest, this approach is generic and framework or language agnostic (even if you read the previous article on applying with Flutter, you would see that). Good architecture and implemented design principles make coding faster, easier to maintain, and make your code more robust, as the colleague said in the other comment.
This approach is a suggestion, not a rule. Of course, most developers split up their code into folder structures that make sense for them and their team, but having a guide can make it easier to make fewer mistakes.
And the comment about "spending a lot of time writing articles", is good for exercising knowledge and sharing with people who are interested. I also write to help me write my master's thesis and practice my English.
It's not all just about code, it's also about planning.

Collapse
duduindo profile image
Eduardo Paixão

Great article! Vlw, mano

Collapse
suheylz profile image
SuheylZ

this is not clean architecture because as Uncle Tom said, it's the framework that is hidding the business for which the app is made. suppose if you have to add the functionality "view customers" how would you work on that?

  • goto src\presentation\component folder to add components
  • goto src\main\routes to add route
  • goto src\main\pages to add the page
  • goto scr\domain\usecases to add interface
  • goto src\domain\models to add the model
  • goto src\data\usecases to add use case

i haven't mentioned test cases. so you see the problem here? I'm constant jumping from one folder to another. when it comes to bug fixing it would be a mess to locate n fix it.

now consider an alternate approach.
i have a folder src\customers
i add sub folder view and have the page, components, reset api, interface, route models and test cases all in one folder. which once do you think is easier

Collapse
xchiro profile image
Jorge "Chiro" Nuñez Delgado • Edited on

To be honest, I don't know why the devs want to use Clean Architecture in a front-end project, the clean architecture is clear, front-end is a detail, not live in the domain, use clean architecture in front-end is redundant, Clean Architecture is amazing but it's a software architecture that said the Front End its a Detail not live in the domain, create a front-end with Clean Architecture is not Clean Architecture, in my opinion.

Collapse
sutt0n profile image
Joseph Sutton

Clean architecture in a front-end environment is possible, as long as you stick with the principles of presenter, view, and the separation of business logic. If you do it correctly enough, you should have zero business logic written in JSX, and it should be easily readable, testable, and maintainable through "computeds" (these can be state-driven, either locally or globally; however, they should be able to be completely extracted out and testable).

There's no right or wrong way; however, if it's a hassle for an engineer, it's probably the wrong approach. Clean architecture with things like Redux or React Context is possible, but difficult. There's tooling out there for different approaches, it's up to the engineer to pick the right one for themselves.

Collapse
xchiro profile image
Jorge "Chiro" Nuñez Delgado • Edited on

Sorry but I do not share the same ideas, Uncle Bob its clear here, the web is a detail, its a delivery mechanism, if your web contains business logic, you are not doing Clean Arch, you can still follow SOLID principles, but not is Clean Arch, React it's a pure front-end tool, I don't see any case where you need to write your uses cases in React/front-end, because the front-end in a correct Clean Arch it is a lower layer, if you are writing your uses cases in front-end, in my opinion, its a bad design, unless, if your web application does not contain a back-en, but that not is a real case.

Thread Thread
sutt0n profile image
Joseph Sutton

You can separate entities and their use cases from the engineering-scoped frontend entirely — the computeds I referenced (thought I typed it out, my apologies) would call these entities and their use-cases.

Thread Thread
rubemfsv profile image
Rubem Vasconcelos Author

Exactly. In cases of working like this with React, if you need to develop an app in React Native, writing your use cases and separate entities saves you a lot of time because you can reuse many parts. This text approach is not perfect, it needs a separate presenter and view layer for better decoupling, but yes, you can use clean architecture in frontend web applications. Uncle Bob talks about framework and language agnostic architectures, and front end situations apply. In the case of the frontend, the web detail can be considered the framework (React, Angular, Vue), and to change the detail, just change the layer that contains information regarding the framework.

Thread Thread
xchiro profile image
Jorge "Chiro" Nuñez Delgado

If you are following clean architecture principles in a web system with back end, you front end only need the MVC pattern. And in the controllers you can set references to your use cases, but again in real world the uses cases lives in the back end not in the front end, in my opinion use clean architecture in tools like React not is necessary in most of the case.