Clean Architecture: TypeScript and React

Search for a command to run...

No comments yet. Be the first to comment.
EnvironmentObject is useful when you want to create a dependency in a higher component of the layout tree and use it on a lower component without having to pass it down the tree through every child component. We will now use EnvironmentObject to moni...

CompositionLocal is useful when you want to create a dependency in a higher node of the layout tree and use it on a lower node without having to pass it down the tree through every child Composable. Here we will use it to direct our application when ...

The TDD (Test Driven Development) approach is important when it comes to testing small portions of business logic code in isolation. In this blog, I'll cover how I would use TDD to build a clean architecture To-do SwiftUI application I'll cover imp...

The TDD (Test Driven Development) approach is important when it comes to testing small portions of business logic code in isolation. In this blog, I'll cover how I would use TDD to build a clean architecture To-do Jetpack Compose Android application ...

The Room persistence library provides an abstraction layer over SQLite. To use Room in your app, add the following dependencies to your app’s build.gradle file: dependencies { .... kapt "org.xerial:sqlite-jdbc:3.34.0" // Room def roo...

By employing clean architecture, you can design applications with very low coupling and independent of technical implementation details, such as databases and frameworks. That way, the application becomes easy to maintain and flexible to change. It also becomes intrinsically testable. Here I’ll show how I structure my clean architecture projects. This time we are going to build a React todo application using Typescript.
The folder/group structure of the project takes on the following form:
├── Core
├── Data
├── Domain
└── Presentation
Let’s start with the Domain Layer.
This layer describes WHAT your project/application does. Let me explain, Many applications are built and structured in a way that you cannot understand what the application does by merely looking at the folder structure. Using a building of a house analogy, you can quickly identify what a building would look like and its functionality by viewing the floor plan and elevation of the building

In the same way, the domain layer of our project should specify and describe WHAT our application does. In this folder, we would use models, repository interfaces, and use cases.
├── Core
├── Data
├── Presentation
└── Domain
├── Model
│ ├── Todo.ts
│ └── User.ts
├── Repository
│ ├── TodoRepository.ts
│ └── UserRepository.ts
└── UseCase
├── Todo
│ ├── GetTodos.ts
│ ├── GetTodo.ts
│ ├── DeleteTodo.ts
│ ├── UpdateTodo.ts
│ └── CreateTodo.ts
└── User
├── GetUsers.ts
├── GetUser.ts
├── DeleteUser.ts
├── UpdateUser.ts
└── CreateUser.ts
The PRESENTATION layer will keep all of the consumer-related code as to HOW the application will interact with the outside world. The presentation layer can be WebForms, Command Line Interface, API Endpoints, etc. In this case, it would be the screens for a List of Todos and its accompanying view model.
├── Core
├── Data
├── Domain
└── Presentation
└── Todo
└── TodoList
├── TodoListViewModel.tsx
└── TodoListView.tsx
The DATA layer will keep all the external dependency-related code as to HOW they are implemented:
├── Core
├── Domain
├── Presentation
└── Data
├── Repository
│ ├── TodoRepositoryImpl.ts
└── DataSource
├── TodoDataSource.ts
├── API
│ ├── TodoAPIDataSourceImpl.ts
│ └── Entity
│ ├── TodoAPIEntity.ts
│ └── UserAPIEntity.ts
└── DB
├── TodoDBDataSourceImpl.ts
└── Entity
├── TodoDBEntity.ts
└── UserDBEntity.ts
and lastly, the CORE layer keep all the components that are common across all layers like constants or configs or dependency injection (which we won’t cover) Our first task would be always to start with the domain models and data entities. Let’s start with the model
//Domain/Model/Todo.ts
export interface Todo {
id: number;
title: string;
isComplete: boolean;
}
We need it to conform to Identifiable as we’re going to display these items in a list view.
Next, let’s do the todo entity
//Data/DataSource/API/Entity/TodoAPIEntity.ts
export interface TodoAPIEntity {
id: number;
title: string;
completed: boolean;
}
Let’s now write an interface for the TodoDatasource
//Data/DataSource/TodoDataSource.ts
import { Todo } from "../../Domain/Model/Todo";
export default interface TodoDataSource {
getTodos(): Promise<Todo[]>;
}
We have enough to write an implementation of this interface and we’ll call it TodoAPIImpl:
import { Todo } from "../../../Domain/Model/Todo";
import TodoDataSource from "../TodoDataSource";
import { TodoAPIEntity } from "./Entity/TodoAPIEntity";
const BASE_URL = "https://jsonplaceholder.typicode.com";
interface TypedResponse<T = any> extends Response {
json<P = T>(): Promise<P>;
}
function myFetch<T>(...args: any): Promise<TypedResponse<T>> {
return fetch.apply(window, args);
}
export default class TodoAPIDataSourceImpl implements TodoDataSource {
async getTodos(): Promise<Todo[]> {
let response = await myFetch<TodoAPIEntity[]>(`${BASE_URL}/todos`);
let data = await response.json();
return data.map((item) => ({
id: item.id,
title: item.title,
isComplete: item.completed,
}));
}
}
Note: this data source's getTodos function returns a list of Todo. So, we have to map TodoEntity -> Todo: Before we write our TodoRepositoryImpl let’s write the interface for that in the Domain layer
//Domain/Repository/TodoRepository.ts
import { Todo } from "../Model/Todo";
export interface TodoRepository {
getTodos(): Promise<Todo[]>;
}
//Data/Repository/TodoRepositoryImpl.ts
import { Todo } from "../../Domain/Model/Todo";
import { TodoRepository } from "../../Domain/Repository/TodoRepository";
import TodoDataSource from "../DataSource/TodoDataSource";
export class TodoRepositoryImpl implements TodoRepository {
dataSource: TodoDataSource;
constructor(_datasource: TodoDataSource) {
this.dataSource = _datasource;
}
async getTodos(): Promise<Todo[]> {
return this.dataSource.getTodos();
}
}
Now that we have our todo repository, we can code up the GetTodos use case
//Domain/UseCase/GetTodos.ts
import { Todo } from "../Model/Todo";
import { TodoRepository } from "../Repository/TodoRepository";
export interface GetTodos {
invoke(): Promise<Todo[]>;
}
export class GetTodosUseCase implements GetTodos {
private todoRepo: TodoRepository;
constructor(_todoRepo: TodoRepository) {
this.todoRepo = _todoRepo;
}
async invoke(): Promise<Todo[]> {
return this.todoRepo.getTodos();
}
}
and then in turn we can write our presentation’s view model and view
//Presentation/View/Todo/TodoListViewModel.ts
import { useState } from "react";
import TodoAPIDataSourceImpl from "../../../Data/DataSource/API/TodoAPIDataSourceImpl";
import { TodoRepositoryImpl } from "../../../Data/Repository/TodoRepositoryImpl";
import { Todo } from "../../../Domain/Model/Todo";
import { GetTodosUseCase } from "../../../Domain/UseCase/GetTodos";
export default function TodoViewModel() {
const [todos, setTodos] = useState<Todo[]>([]);
const UseCase = new GetTodosUseCase(
new TodoRepositoryImpl(new TodoAPIDataSourceImpl())
);
async function getTodos() {
setTodos(await UseCase.invoke());
}
return {
getTodos,
todos,
};
}
//Presentation/Views/Todo/TodoListView.ts
import { useEffect } from "react";
import useViewModel from "./TodoListViewModel";
import {
List,
ListItem,
ListItemIcon,
Checkbox,
ListItemText,
} from "@mui/material";
export default function TodoView() {
const { getTodos, todos } = useViewModel();
useEffect(() => {
getTodos();
}, []);
return (
<List>
{todos.map((todo, i) => {
return (
<ListItem key={i}>
<ListItemIcon>
<Checkbox checked={todo.isComplete} />
</ListItemIcon>
<ListItemText primary={todo.title} />
</ListItem>
);
})}
</List>
);
}

So to recap:
