The Clean Architecture using React and TypeScript. Part 1: Basics

Rostislav Dugin
20 min readMay 17, 2020

--

In this article we will talk about software architecture in web development. For quite a long time, me and my colleagues have been using variation of The Clean Architecture for building architecture in our frontend projects. Initially, I started to use it, when I started to use TypeScript, because I couldn’t find other suitable common architectural approaches in the world of development on React (by the way, I came from Android development where long time ago, before Kotlin, Fernando Cejas made a fuss with an article, which I still sometimes refer to).

I want to tell you about our experience of using an architecture approach called The Clean Architecture in React applications with TypeScript. Why am I describing this? — Sometimes I need to explain and justify its use to developers who are not yet familiar with this approach. So here I will make a detailed analysis with visual explanations that I can refer to in the future.

Table of content
1. Introduction
2. Theoretical part
— 2.1. Why should we use architecture?
— 2.2. Original definition of The Clean Architecture
— 2.3. The Clean Architecture for frontend
3. Practical part
— 3.1. Description of authorization application
— 3.2. Source code Structure
— 3.3. UML project diagram
— 3.4. Code explanation
4. Conclusion
5. Resources

1. Introduction

Architecture — is a global thing. Its understanding is necessary not only in the context of a particular programming language. You need to understand key ideas in general, in order to understand why and how core benefits of architecture (any architecture) using are archived. The approach is the same as with design or SOLID patterns — they are invented not for a concrete language, they work with the entire programming methodologies (for example, OOP).

Architecture is easier to understand if you see the whole picture. That is why in this article I will describe not only how “in theory” should be, but also give you a concrete example of a project. First of all, we will study the theoretical side of applying The Clean Architecture in frontend, and then we will look at a web application with a UML diagram and description of each class.

Important note: The Clean Architecture does not give you strict rules how to organize your applications, it only provides you with recommendations. Each platform and language will have its own details. This article shows the approach that I have been using with my colleagues, but keep in mind — it is not only one possible way of architecture using.

I would also like to note that using such architectural approaches may become overengeneering for small projects. The main goal of any architecture is to make the code clear, maintainable and testable. If your application is faster to write on pure JS without architectures, testing, etc. — it is quite normal. Do not use it where you do not really need it. Remember that an architecture\testing is mainly used in middle-large projects with several developers, where you need to read and change someone else’s code often.

There are many other approaches and described architecture approach can be improved and extended. The key idea of this article and the main reason why we use it is — this architecture works for us, holds large projects and it is understandable for other developers. So feel free to add new rules to it and change something as you need, but the main thing — is to keep it working and understandable. Do not use architecture just to use architecture.

The following quote has something in common with idea of overusing architectures:

Premature optimization is the root of all evil (or at least most of it) in programming.

— Donald Knuth, “Computer Programming as an Art” (1974).

So do not try to create something perfect when you just need to solve the problem (in our case, to solve the problem of maintenance at the expense of architecture).

2. Theoretical part

2.1. Why do we use architecture?

Answer: architecture is needed to save time during the development process, to maintain the system’s testability and extensibility over a long development period.

To learn more about what happens if you do not use anarchitecture for large applications, you can read, for example, Bob Martin’s book “The Clean Architecture”. For a brief explanation, I will give you the following chart from this book:

On the chart, we can see that with each new version (let’s say they are released at equal intervals), fewer and fewer lines of code are added to the system. However the cost of a line of code is growing. This is due to the complexity of the system, and changes are beginning to require an unreasonably large amount of effort.

In The Clean Architecture book, this chart is given as an example of a bad architecture. This approach will end up extending the system is more expensive than the benefit of the system itself.

And now look at my “ideal” example, which we (developers, PMs, customers) would like to see in our projects:

The chart shows that the growth rate of lines does not change with time. The cost of a line of code (depending on the version) increases, but slightly, taking into account the fact that we are talking about millions of lines. Unfortunately, such rate is not real if we speak about large enterprise systems, because the product is expanding, the complexity of the system is increasing, developers are changing, so development cost will inevitably increase.

However, I can make you happy — we are talking about frontend applications! Let’s face it — as a rule, such applications don’t grow to millions of lines, otherwise browsers would have downloaded such applications for an enormously long time. If application is too big, it is separated into different products, and the basic logic is placed on the backend side. That is why we can, at least, try to strive to the above-mentioned trend of increasing the cost of code (with variable success, depending on the size of the application). If our project is even 50% cheaper to maintain than it could be without a good architecture — it will save developers’ time and customer’s profit.

Building a good and clear architecture from the start of the project gives you the following benefits:

  • cheaper code maintenance (less time is required and cheaper financial costs);
  • simplification of code testing (you will need fewer testers and have fewer missed “production bugs”);
  • easier to involve new developers into the project.

When I was writing an article, I faced the following quote (I completely agree with it):

“The remaining 10 percent of the code accounts for the other 90 percent of the development time.”

— Tom Cargill, Bell Labs

Conclusion: the system is more expensive to change that to create from start, so you need to think how you will change it in the future

I think I have answered the question “what is it for?”. Now, let’s move to the technical part of the question.

2.2. Original definition of the Clean Architecture

I will not go into details of The Clean Architecture, as this topic is covered in many articles, but I will briefly formulate short explanation.

The original 2012 article by Bob Martin shows the following diagram:

The key idea behind this diagram is that the application should be divided into layers (there can be any amount of layers). The inner layers do not know about outer ones, the dependencies are directed to the center. The far a layer is from the center, the more it knows about “non-business” details of the application (e.g. which framework is used and how many buttons on the screen).

  • Entities. We have Entities in the center. They contain the business logic of the application and there are no platform dependencies. Entities describe the application’s business logic only. For example, take the “cart” class — we can add a product to the cart, remove it, etc. Nothing about such things as React, databases, buttons this class does not know.
    Speaking about platform independence, we mean that specific libraries like React\Angular\Express\Nest.js\DI etc. are not used here. If necessary, for example, we can take an entire entity from a Web application based on React — and paste it into code for NodeJS without changes.
  • Use cases. The second layer of the diagram contains use cases (aka Interactors). Use cases describe how to interact with the entities in the context of our application. For example, if the entity “cart” knows only that an order can be added to it, the use case knows that it can take that order from the “cart” and send it to the repository (see below).
  • Gateways, Presenters, etc. In this context (Gateways = Repositories, Presenters = View Models) — layers of the system, which are responsible for the relationship between the business rules of the application and platform dependent parts of the system. For example, repositories provide interfaces that will implement classes to access APIs or repositories, and the View Model interface will be used to link React components with business logic calls.
    Note: in our case, Use Cases and Repositories will usually be in a different order, because the most of the work of frontend applications is to receive and send data through API (depending on the project).
  • External interfaces. Platform dependent layer. Here you can see direct access to the API, React components, etc. This is the layer most difficult to test and to build abstraction (a button in React — is a button in React, there is no abstraction).

2.3. The Clean Architecture for frontend

Now let’s move on to our frontend area. In the context of Frontend, the diagram above can be view like this:

  • Entities. Business entities are the same as in the original version of the architecture. Note that Entities know how to store a state and are often used for this purpose. For example, an entity “cart” can store orders of the current session to provide methods of working with them (getting the total price, the total amount of goods, etc.).
  • Repository interfaces. Interfaces to access the API, database, storages, etc. It may seem strange that interfaces for data access are “above” use cases. However, practice shows that use cases know about repositories and actively use them — repositories do not know anything about use cases, but they do know about entities. This is an example of dependency inversion from SOLID (the ability to define an interface in the inner layer by making an implementation in the outer layer). Using interfaces adds abstraction (e.g. nobody knows if the repository makes API queries or takes data from the cache).
  • Use Cases. Similar to the original diagram. Objects that implement business logic in the context of our application (i.e. understand what to do with entities — send, upload, filter, merge).
  • View Models and View Interfaces
    View Model — is a replacement for Presenters from the original chart. In our projects we use MVVP architecture instead of MVP\MVC\MV*. In brief, the difference with MVVP is only one: Presenter knows about View and calls its methods, but ViewModel doesn’t know about View, having only one method to notify about changes. View simply “monitors” the state of the View Model. MVVP is unidirectional (View → ViewModel), while MVP is bidirectional (View ︎← → Presenter). Fewer dependencies — are easier to test.
    View Interfaces — in our case, one base class for all Views, through which View Model notifies specific View implementations about changes. It contains the onViewModelChanged() method. This is one more example of dependency inversion.
  • External interfaces. Like on the original diagram, this layer contains platform-dependent implementations. In the case of the application below — these are React components and implementations of interfaces to access the API. However, here can also be any other framework (AngularJS, React Native) and any other storage (IndexDB, local storage, etc.). The Clean Architecture allows you to isolate the use of specific frameworks, libraries and technologies, therefore you can test them easier and replace them to another ones.

If we show at the diagram above as a three-layer application, it takes the following form:

The red arrows show the flow of data (but not the dependencies, the dependency diagram is shown on the circle chart above). Picture as a rectangular diagram allows you to better understand how the data flow within the application is moving. I saw the idea of describing the idea as a diagram in THIS article.

Keep in mind that the structure of layers may change in more complex applications. For example, it is a common practice when each layer that is higher than the domain has own data mappers.

3. Practical part

3.1. Description of authorization application

In order to make the architecture more understandable and clear, I created a web application built on its base. You can see the source code of the application in the GitHub repository. The application looks like this:

The application respresents a simple authorization window. To make the application more complicated (in order to make the architecture useful), we consider the following functions:

  • Fields should not be empty (validation).
  • The entered email must be in the correct format (validation).
  • The access data must be validated on the server (there is API stub) and receive the validation key.
  • Data validation and validation key must be provided from the API method for authorization.
  • After authorization, the access key must be saved inside the application (entity layer).
  • The authorization key must be removed from the memory when it exits.

3.2. Source code structure

In our example, the structure of the src folder looks like this:

  • data — contains classes for accessing the data. This directory is the final layer on the circle chart, because it contains classes for implementing repository interfaces. Therefore, these classes know about API and platform dependent things (local storage, cookies, etc.).
  • domain — these classes are classes of business logic. Here you will find Entities, Use Cases and Repository Interfaces. The entities subdirectory is divided into two directories: models and structures. The difference between these directories is that models are entities with logic and structures are simple data structures (like POJO in Java). This division is made for convenience, in models we put classes with which we (developers) directly and often work, and in structures — objects that are returned by the server as JSON-objects (hello, json2ts) and we use them to transfer between layers.
  • presentation — contains View Models, View Interfaces and View (framework components). Also it contains util for various validations, utilities, etc.

Of course, the structure can change from project to project. For example, in my project there are classes for controlling the state of a sidebar and a class for navigating between pages.

3.3. UML project diagram

If you want to zoom it, you can find sources on GitHub.

The division of classes into layers is shown by rectangles borders. Note that dependencies are directed to the Domain layer (according to the diagram).

3.4. Code explanation

Entities layer
In this section we will walk through all classes with description of their logic. We will start from the first inner circle — Entities, because other classes are based on it.

AuthListener.tsx

// This class is used to update listeners
// in the AuthHolder class
export default interface AuthListener {
onAuthChanged(): void;
}

AuthHolder.tsx

import AuthListener from './AuthListener';// This class holds autorization state (see point 3.1.5).
// In order to update presentation layer, we use an "Observer"
// pattern with AuthListener listener
export default class AuthHolder {
private authListeners: AuthListener[];
private isAuthorized: boolean;
private authToken: string;

public constructor() {
this.isAuthorized = false;
this.authListeners = [];
this.authToken = '';
}

public onSignedIn(authToken: string): void {
this.isAuthorized = true;
this.authToken = authToken;
this.notifyListeners();
}

public onSignedOut(): void {
this.isAuthorized = false;
this.authToken = '';
this.notifyListeners();
}

public isUserAuthorized(): boolean {
return this.isAuthorized;
}

/**
* @throws {Error} if user is not authorized
*/
public getAuthToken(): string {
if (!this.isAuthorized) {
throw new Error('User is not authorized');
}

return this.authToken;
}

public addAuthListener(authListener: AuthListener): void {
this.authListeners.push(authListener);
}

public removeAuthListener(authListener: AuthListener): void {
this.authListeners.splice(this.authListeners.indexOf(authListener), 1);
}

private notifyListeners(): void {
this.authListeners.forEach((listener) => listener.onAuthChanged());
}
}

AuthorizationResult.tsx

// A simple data structure for transferring between layers
export default interface AuthorizationResult {
authorizationToken: string;
}

ValidationResult.tsx

// One more simple structure for transferring between layers
export default interface ValidationResult {
validationKey: string;
}

This is the end of the entity layer. Note that this layer deals only with business logic (state storage) and it is used to transfer data throughout the application.

Often, the state does not need to be stored in business logic classes. For this case you can use repository with use case or view model.

Repository interfaces
AuthRepository.tsx

import ValidationResult from '../../entity/auth/stuctures/ValidationResult';
import AuthorizationResult from '../../entity/auth/stuctures/AuthorizationResult';
// Here we define an interface that will be implemented to access API
export default interface AuthRepository {
/**
* @throws {Error} if validation has not passed
*/
validateCredentials(email: string, password: string): Promise<ValidationResult>;

/**
* @throws {Error} if credentials have not passed
*/
login(email: string, password: string, validationKey: string): Promise<AuthorizationResult>;
}

Use Cases
LoginUseCase.tsx

import AuthRepository from '../../repository/auth/AuthRepository';
import AuthHolder from '../../entity/auth/models/AuthHolder';

export default class LoginUseCase {
private authRepository: AuthRepository;
private authHolder: AuthHolder;

public constructor(authRepository: AuthRepository, authHolder: AuthHolder) {
this.authRepository = authRepository;
this.authHolder = authHolder;
}

/**
* @throws {Error} if credentials are not valid or have not passed
*/
public async loginUser(email: string, password: string): Promise<void> {
const validationResult = await this.authRepository.validateCredentials(email, password);
const authResult = await this.authRepository.login(
email,
password,
validationResult.validationKey,
);

this.authHolder.onSignedIn(authResult.authorizationToken);
}
}

In this example the use case has only one method. Usually use cases have only one public method, which implements complex logic for one action. In this case — you should perform validation and then send validation data to the API method of authorization.

However, the approach when several use cases are combined into one if they share the same logic is also often used.

Make sure that use cases do not contain logic that should be in entities. Too many methods or storing the state in the use case often indicate that the code should be in another layer.

Repository implementation
AuthFakeApi.tsx

import AuthRepository from '../../domain/repository/auth/AuthRepository';
import ValidationResult from '../../domain/entity/auth/stuctures/ValidationResult';
import AuthorizationResult from '../../domain/entity/auth/stuctures/AuthorizationResult';
// Class that imitates access to the API
export default class AuthFakeApi implements AuthRepository {
/**
* @throws {Error} if validation has not passed
*/
validateCredentials(email: string, password: string): Promise<ValidationResult> {
return new Promise((resolve, reject) => {
// Here we define a rult that the server should support
if (password.length < 5) {
reject(new Error('Password length should be more than 5 characters'));
return;
}

resolve({
validationKey: 'A34dZ7',
});
});
}

/**
* @throws {Error} if credentials have not passed
*/
login(email: string, password: string, validationKey: string): Promise<AuthorizationResult> {
return new Promise((resolve, reject) => {
// Here we imitate a validation key verification
if (validationKey === 'A34dZ7') {
// Create stub for account with login "user@email.com" and password "password"
if (email === 'user@email.com' && password === 'password') {
resolve({
authorizationToken: 'Bearer ASKJdsfjdijosd93wiesf93isef',
});
}
} else {
reject(new Error('Validation key is not correct. Please try later'));
return;
}

reject(new Error('Email or password is not correct'));
});
}
}

In this class we have implemented a simulation of API access. We return the Promise, which would return a real fetch request. If we want to replace the implementation with a real API, we just change the AuthFakeApi class to AuthApi in App.tsx file or the dependency injection tool, if used.

Note that we will annotate methods with error description so that other programmers understand the need of error handling. Unfortunately, TypeScript does not currently have instructions like @throws in Java, so we use a simple annotation.

util (presentation layer)
In this directory we put classes that implement the logic of “preventive” data validation, as well as other classes that help to with the UI layer.

FormValidator.tsx

export default class FormValidator {
static isValidEmail(email: string): boolean {
const emailRegex = /^\S+@\S+\.\S+$/;
return emailRegex.test(email);
}
}

View interfaces

BaseView.tsx
A class that allows View Model to notify View about changes. It is implemented by all View components.

export default interface BaseView {
onViewModelChanged(): void;
}

ViewModels

BaseViewModel.tsx
An interface that provides methods for linking View Model and View. It is implemented by all View Models.

import BaseView from '../view/BaseView';

export default interface BaseViewModel {
attachView(baseView: BaseView): void;
detachView(): void;
}

AuthViewModel.tsx

import BaseViewModel from '../BaseViewModel';// This is an interface of ViewModel that will be available
// for View. Here we define all public fields that View will
// be using
export default interface AuthViewModel extends BaseViewModel {
emailQuery: string;
passwordQuery: string;
isSignInButtonVisible: boolean;
isSignOutButtonVisible: boolean;

isShowError: boolean;
errorMessage: string;

authStatus: string;
isAuthStatusPositive: boolean;

onEmailQueryChanged(loginQuery: string): void;
onPasswordQueryChanged(passwordQuery: string): void;
onClickSignIn(): void;
onClickSignOut(): void;
}

AuthViewModelImpl.tsx

import AuthViewModel from './AuthViewModel';
import BaseView from '../../view/BaseView';
import LoginUseCase from '../../../domain/interactors/auth/LoginUseCase';
import AuthHolder from '../../../domain/entity/auth/models/AuthHolder';
import AuthListener from '../../../domain/entity/auth/models/AuthListener';
import FormValidator from '../../util/FormValidator';

export default class AuthViewModelImpl implements AuthViewModel, AuthListener {
public emailQuery: string;
public passwordQuery: string;
public isSignInButtonVisible: boolean;
public isSignOutButtonVisible: boolean;

public isShowError: boolean;
public errorMessage: string;

public authStatus: string;
public isAuthStatusPositive: boolean;

private baseView?: BaseView;
private loginUseCase: LoginUseCase;
private authHolder: AuthHolder;

public constructor(loginUseCase: LoginUseCase, authHolder: AuthHolder) {
this.emailQuery = '';
this.passwordQuery = '';
this.isSignInButtonVisible = true;
this.isSignOutButtonVisible = false;

this.isShowError = false;
this.errorMessage = '';

this.authStatus = 'is not authorized';
this.isAuthStatusPositive = false;

this.loginUseCase = loginUseCase;
this.authHolder = authHolder;
// We set current class as a listener of auth events
this.authHolder.addAuthListener(this);
}

public attachView = (baseView: BaseView): void => {
this.baseView = baseView;
};

public detachView = (): void => {
this.baseView = undefined;
};
// This is method from AuthListener interface
public onAuthChanged = (): void => {
// We change data of the model to make
// View display changes on login and logout
if (this.authHolder.isUserAuthorized()) {
this.isSignInButtonVisible = false;
this.isSignOutButtonVisible = true;
this.authStatus = 'authorized';
this.isAuthStatusPositive = true;
} else {
this.isSignInButtonVisible = true;
this.isSignOutButtonVisible = false;
this.authStatus = 'is not autorized';
this.isAuthStatusPositive = false;
}

this.notifyViewAboutChanges();
};

public onEmailQueryChanged = (loginQuery: string): void => {
this.emailQuery = loginQuery;
this.notifyViewAboutChanges();
};

public onPasswordQueryChanged = (passwordQuery: string): void => {
this.passwordQuery = passwordQuery;
this.notifyViewAboutChanges();
};

public onClickSignIn = async (): Promise<void> => {
if (!this.validateLoginForm()) {
this.notifyViewAboutChanges();
return;
}

try {
await this.loginUseCase.loginUser(this.emailQuery, this.passwordQuery);
this.isShowError = false;
this.errorMessage = '';
} catch (e) {
this.errorMessage = e.message;
this.isShowError = true;
}

this.notifyViewAboutChanges();
};

public onClickSignOut = (): void => {
// We delete auth data without intermediatiors
// like use cases
this.authHolder.onSignedOut();
};

private validateLoginForm = (): boolean => {
if (!this.emailQuery) {
this.isShowError = true;
this.errorMessage = 'Email cannot be empty';
return false;
}
// We remove the error if it was set for this condition
if (this.errorMessage === 'Email cannot be empty') {
this.isShowError = false;
this.errorMessage = '';
}

if (!FormValidator.isValidEmail(this.emailQuery)) {
this.isShowError = true;
this.errorMessage = 'Email format is not valid';
return false;
}
if (this.errorMessage === 'Email format is not valid') {
this.isShowError = false;
this.errorMessage = '';
}

if (!this.passwordQuery) {
this.isShowError = true;
this.errorMessage = 'Password cannot be empty';
return false;
}
if (this.errorMessage === 'Password cannot be empty') {
this.isShowError = false;
this.errorMessage = '';
}

return true;
}

private notifyViewAboutChanges = (): void => {
if (this.baseView) {
this.baseView.onViewModelChanged();
}
};
}

Look at the onClickSignOut method where we directly refer to the AuthHolder class. This is one of those cases where a use case would not be useful, because the logic of the method is quite simple. You can similarly refer directly to the interface of the repositories.

However, if the code becomes more complex, you need to put it into a use case.

UI (views)
AuthComponent.tsx

import React from 'react';
import './auth-component.css';
import BaseView from '../BaseView';
import AuthViewModel from '../../view-model/auth/AuthViewModel';

export interface AuthComponentProps {
authViewModel: AuthViewModel;
}

export interface AuthComponentState {
emailQuery: string;
passwordQuery: string;
isSignInButtonVisible: boolean;
isSignOutButtonVisible: boolean;

isShowError: boolean;
errorMessage: string;

authStatus: string;
isAuthStatusPositive: boolean;
}

export default class AuthComponent extends React.Component<AuthComponentProps, AuthComponentState>
implements BaseView {
private authViewModel: AuthViewModel;

public constructor(props: AuthComponentProps) {
super(props);

const { authViewModel } = this.props;
this.authViewModel = authViewModel;

this.state = {
emailQuery: authViewModel.emailQuery,
passwordQuery: authViewModel.passwordQuery,
isSignInButtonVisible: authViewModel.isSignInButtonVisible,
isSignOutButtonVisible: authViewModel.isSignOutButtonVisible,

isShowError: authViewModel.isShowError,
errorMessage: authViewModel.errorMessage,

authStatus: authViewModel.authStatus,
isAuthStatusPositive: authViewModel.isAuthStatusPositive,
};
}

public componentDidMount(): void {
this.authViewModel.attachView(this);
}

public componentWillUnmount(): void {
this.authViewModel.detachView();
}

// We update state of our component
// on each update of ViewModel
public onViewModelChanged(): void {
this.setState({
emailQuery: this.authViewModel.emailQuery,
passwordQuery: this.authViewModel.passwordQuery,
isSignInButtonVisible: this.authViewModel.isSignInButtonVisible,
isSignOutButtonVisible: this.authViewModel.isSignOutButtonVisible,

isShowError: this.authViewModel.isShowError,
errorMessage: this.authViewModel.errorMessage,

authStatus: this.authViewModel.authStatus,
isAuthStatusPositive: this.authViewModel.isAuthStatusPositive,
});
}

public render(): JSX.Element {
const {
emailQuery,
passwordQuery,
isSignInButtonVisible,
isSignOutButtonVisible,

isShowError,
errorMessage,

authStatus,
isAuthStatusPositive,
} = this.state;

return (
<div className="row flex-grow-1 d-flex justify-content-center align-items-center">
<div className="auth-container col bg-white border rounded-lg py-4 px-5">
<div className="row mt-2 mb-4">
Status:&nbsp;
<span className={`${isAuthStatusPositive ? 'text-success' : 'text-danger'}`}>
{authStatus}
</span>
</div>

<div className="row mt-2">
<input
type="text"
placeholder="user@email.com"
onChange={(e: React.FormEvent<HTMLInputElement>): void => {
this.authViewModel.onEmailQueryChanged(e.currentTarget.value);
}}
value={emailQuery}
className="form-control"
/>
</div>
<div className="row mt-2">
<input
type="password"
placeholder="password"
onChange={(e: React.FormEvent<HTMLInputElement>): void => {
this.authViewModel.onPasswordQueryChanged(e.currentTarget.value);
}}
value={passwordQuery}
className="form-control"
/>
</div>

{isShowError && (
<div className="row my-3 text-danger justify-content-center">{errorMessage}</div>
)}

{isSignInButtonVisible && (
<div className="row mt-4">
<button
type="button"
className="col btn btn-primary"
onClick={(): void => this.authViewModel.onClickSignIn()}
>
Sign in
</button>
</div>
)}

{isSignOutButtonVisible && (
<div className="row mt-4">
<button
type="button"
className="col btn btn-primary"
onClick={(): void => this.authViewModel.onClickSignOut()}
>
Sign out
</button>
</div>
)}
</div>
</div>
);
}
}

This component is framework dependent and therefore it is located in the final layer of the diagram.

When AuthComponent mounts (componentDidMount), it is attached to the AuthViewModel and detached when it disappears (componentWillUnmount). Each time the ViewModel is changed, AuthComponent updates its status in order to update the layout.

Look at the conditional rendering based on the condition:

{isSignOutButtonVisible && (
<div className="row mt-4">
<button
type="button"
className="col btn btn-primary"
onClick={(): void => this.authViewModel.onClickSignOut()}
>
Sign out
</button>
</div>
)}

Also look at the use of methods from ViewModel to transfer values:

onClick={(): void => this.authViewModel.onClickSignOut()}

Entry point

To enter the application, we use index.tsx and App.tsx files.

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css';

import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

App.tsx

import React from 'react';
import './App.css';
import AuthComponent from './presentation/view/auth/AuthComponent';
import AuthViewModelImpl from './presentation/view-model/auth/AuthViewModelImpl';
import AuthFakeApi from './data/auth/AuthFakeApi';
import LoginUseCase from './domain/interactors/auth/LoginUseCase';
import AuthHolder from './domain/entity/auth/models/AuthHolder';

function App(): JSX.Element {
// data layer
const authRepository = new AuthFakeApi();
// domain layer
const authHolder = new AuthHolder();
const loginUseCase = new LoginUseCase(authRepository, authHolder);
// view layer
const authViewModel = new AuthViewModelImpl(loginUseCase, authHolder);

return (
<div className="app-container d-flex container-fluid">
<AuthComponent authViewModel={authViewModel} />
</div>
);
}

export default App;

In the App.tsx file all dependencies are initialized. In this application, we do not use dependency injection tools in order to avoid unnecessary code complicity.

If we need to change some dependencies, we will replace them in this file. For instance, instead of a string:

const authRepository = new AuthFakeApi();

We will write:

const authRepository = new AuthApi();

Also keep in mind that we define interfaces only, not implementations (because all is based on abstraction). When we are declaring variables, we consider the following:

const authRepository: AuthRepository = new AuthFakeApi();

This allows us to hide the details of the implementation (therefore, we can replace them without changing the interface).

4. Conclusion

I hope that you have studied how The Clean Architecture can be applied with React (and not only React), and our experience will help you to make your applications better.

This article describes theoretical and practical basics of using The Clean Architecture in frontend projects. As it was mentioned earlier, The Clean Architecture gives only recommendations about how to build your architecture.

An example of a simple application that uses this architecture has been given here. Keep in mind that the architecture may change as the application grows, so the code above is not the only possible use, this article is just an explanation of our experience.

In future articles I will talk about using of things like testing, libraries (for example, MobX), etc. in The Clean Architecture.

5. Resources

Source code
UML diagramm

--

--