Build a React Application With Clean Architecture

One that actually adheres to Clean Architecture

Motivation

There are multiple React/Clean Architecture applications out there: courses, example repos, and stories. Then, why is writing a new article necessary?

Unfortunately, I'm not satisfied with the available resources related to Clean Architecture. Most of them over-complicate an already obscure subject.

Not only do they lack clarity, they often use the wrong language, or disregard basic rules of Clean Architecture. In this article, I'll do everything possible to provide you with a clear, and valid, implementation of Clean Architecture for React.

What is Clean Architecture

If you stumbled upon this article, you probably know about Clean Architecture, but are looking for more information.

In the rare event of a beginner reading this article, I highly recommend getting started with the necessary theory.

What are we going to build

As we are building a React application, we will create a Clean Architecture version of the Tic-Tac-Toe tutorial (in TypeScript). It's also a pretty common example, making it easy to compare with other codebases.

If you want to follow along with me, you can clone my starter project. There are a few differences with the official Tic-Tac-Toe from React, mainly:

  • End-to-end tests are written with Cypress

  • There is an additional status "draw"

Behaviors

You can find the Tic-Tac-Toe on CodeSandbox to understand what the application is doing in practice. There are two main functionalities:

  • Obviously, you can play a game of Tic-Tac-Toe.

  • You can go back to a previous move (history) and play again.

Files

There are three main files:

App

Board

Square

Clean Architecture implementation

File structure

Before getting started, we can properly set up our file structure, following the idea of Screaming Architecture:

Here, instead of having everything under src/, I define three sub-directories:

  • common/ is empty for now, but will store basic code related to Clean Architecture

  • core/ defines the entry point of our application:

    • The code previously inside main.tsx is now inside bootstrap.tsx.

    • The role of the root main.tsx is now to call bootstrap.

  • tic-tac-toe/ contains everything related to the game itself.

Define a Game Model

While this is not strictly a part of Clean Architecture but Domain-Driven-Design, I like to start by defining a clear Domain Model. Its goal is to define the elements that make up our system.

For our game of Tic-Tac-Toe, we need:

  • A board composed of 9 squares.

  • Players and winners.

  • At one point in time, the state of our game:

    • Is there a winner?

    • The history of moves (so we can navigate among them).

    • Who is playing next?

    • Potentially, the index of the last game played.

I'll create this file under tic-tac-toe/, using a namespace to make it clear:

Create the base structure for Clean Architecture

I'm pretty sure we will create an entity and use case in a minute. We can create the base classes for them under common/:

There are multiple ways to define those.

For Entity, I like to store properties inside a _data object and make it protected. It prevents developers from mutating it directly, which is a common source of issues. Depending on our needs, this class will definitely change.

There are multiple practices when it comes to use cases, but a very common one is to provide an execute method, which returns a Promise most of the time.

Create the Board Entity

Now, we should be able to create the domain for our Tic-Tac-Toe. Everything seems to revolve around the board, which has a lot of potential as an entity!

I'll create a sub-folder inside tic-tac-toe/ called entities/ and add my board.entity.ts.

Here:

  • We store squares, based on the types from our GameDomainModel.

    • We add a getter to easily access the property.
  • We already have the formula to calculate if player X plays next.

    • We copy it from the app.ts file inside the starter project.

    • We can calculate the current step based on the filled squares.

  • The entity is also a great place to calculate if there is a winner.

We can also copy the formula from the starter app.ts file.

In "Clean Architecture", Uncle Bob says:

An Entity is an object within our computer system that embodies a small set of critical business rules operating on Critical Business Data.

In our context, our critical business rules are the rules of the game of Tic-Tac-Toe.

Starting with our first use case

Now, we have everything we need to create an empty use case. We can get started with an obvious one, playing a move.

Obviously, it will take the square we play on. On the other hand, we also need to define the step we play on, as we can play on a previous move. Our use case will probably return an object similar to the game state:

We could probably use GameDomainModel.GameState as Output here. On the other hand, those objects may only seem familiar, and change for different reasons.

If this use case receives the step to play on, that means we need to retrieve the history of played moves. Maybe you guessed it, we need to define a repository!

Add the board repository

For our use case to work, we need a solution to modify the history of played moves, but also retrieve it. First, I'll define a port that contains the necessary interface.

I'll create a sub-directory ports/ under tic-tac-toe/ and add a file called board-repository.port.ts:

Then, we can define an adapter. I don't plan to use some complex storage system, a simple in-memory implementation is enough. I'll also create an adapters/ sub-directory under tic-tac-toe/, and add in-memory-board.repository.ts:

Before we go back to our use case, we need one more thing: exceptions.

Exception management

We are going to retrieve the history of moves, and play on it, but what's going to happen if the step we are supposed to play on doesn't exist?

Also, what if we play on a square that's already taken?

Let's define exceptions for those two, stored inside an exceptions/ sub-directory (itself inside tic-tac-toe/):

Complete our use case

Now that we have our entity, repository, and exceptions, we can complete our use case. When we play a move, we need to:

  • Retrieve the move history

  • Find the board at the given step (and throw an error if it doesn't exist)

  • If there is already a winner, return the game state

  • Otherwise, create a new board, with the move just played (and throw an error if the square is already taken)

  • Return the game state

This use case was added under tic-tac-toe/ in its own sub-directory use-cases/.

Adding missing use cases

Next, we need the ability to go back to a previous move and retrieve the related game state. To do so, I'll add a second use case, called jump-to.use-case.ts.

It's responsible for:

  • Retrieving the move history.

  • Getting the board that corresponds to the given step (or throw an error).

  • Calculate and return the game state.

We are almost done with use cases, but we are still missing one. Currently, we have no solutions to start a new game or retrieve the game state if we ever lose it.

To do so, we need a solution to initialise our board, or more precisely:

  • Retrieve the history of moves.

  • If it's empty, return an empty board and related game state.

  • Otherwise, return the history of moves, and the game state for the last move.

I'll create initialize.use-case.ts with the following:

Getting started with React components

Now that our use cases are completed, we should be able to create our React components and use them.

Before looking at the game itself, I would like to update the Board component. Mainly, let's remove any type of logic it contains, and receive props instead:

Uncle Bob refers to Humble Objects .

Now, we should be able to create a Game component, I'll do it inside a game.tsx file under tic-tac-toe/:

Same thing here, we aren't supposed to have any kind of logic inside our components. On the other hand, we need a solution to orchestrate the actions taken on our view with use cases.

This is the role of a controller.

Reminder: components belong to the UI layer, and use controllers to orchestrate actions.

A great solution is to use hooks! Let's create a game-controller.ts inside tic-tac-toe. It's supposed to provide:

  • The current list of squares.

  • The game status (player's turn or winner/draw).

  • The list of moves to display.

  • A callback when a square is clicked, to play.

  • A callback when a move (to jump to) is clicked.

Also, if you paid attention, maybe you saw I introduced a change compared to the official tutorial: actions are now asynchronous.

Among other things, that means we need to load the game before we can play. We'll introduce a new component later on but, for now, the controller will take a defaultGame option.

This controller is almost complete, but is missing one element, can you see which one? If you answered the repository, you were right!

We are supposed to have a single instance of our repository, so we can't instantiate it in our controller. A great solution would be to introduce some sort of Dependency Injection, but how to do it with React?

Setting up Dependency Injection

A great solution is to use a Context Provider. To avoid files under tic-tac-toe/ to depend on core, I'll create the context inside common/, in dependency-context.ts:

Then, inside core/, I'll create a dependency-context-provider.tsx:

Finally, we can add the DependencyContextProvider to our application, in bootstrap.tsx:

Now, we can go back to our controller, and:

  • Retrieve the dependencies with useDependencyContext.

  • Use the injected repository to initialize our use cases.

  • Additionally, move the use cases to a useMemo to avoid re-creating them.

Using a presenter

If you look closely at our current controller, do you think it's only responsible for orchestration? Or is it doing more than expected?

Obviously, if I'm asking this question, the answer is yes. Currently, we are:

  • Building the structure of moves to be displayed.

  • Calculating the status.

  • Additionally, calculating the list of squares.

You already saw the title of this sub-section, this is the responsibility of a presenter. There are multiple ways to define a presenter. A common practice is to define a class with a format method.

Let's create presenter.ts inside common/:

Now, we need to define our actual presenter. It's supposed to present our game and return the related squares, moves, and status. Based on our current code, it needs the current history, winner, xIsNext, and step.

A controller shouldn't depend directly on a presenter but on its interface.

Let's create a game-presenter.port.ts inside tic-tac-toe/ports/:

Now, we can add the implementation, as game.presenter.ts inside tic-tac-toe/adapters/:

Completing controller

Now that we have a proper presenter, we can:

  • Expect it as a parameter in our controller, so it doesn't directly depend on it.

  • Use it to generate moves, status, and squares.

  • Return them along with onPlay and onJump.

Complete the game component

Now that our controller is done, we need to actually use it inside our Game component. Also, we are supposed to receive a default game (so we can handle the initialization at a higher level), and instantiate the presenter:

Creating a home component

We are missing a single piece for our Tic-Tac-Toe to work: initialization. We need a component that will display a loader while the Initializeuse case is being executed.

Then, once the game state is retrieved, the Game component can actually be displayed. I'll call this component Home, and start with the home-controller.ts inside tic-tac-toe/:

Then, I can add the home.tsx component, also under tic-tac-toe/:

Finally, we need to use our new Home component inside bootstrap.tsx instead of the App by default:

Conclusion

You now have a working Tic-Tac-Toe game, made with React, that adheres to Clean Architecture. The domain (logic) is completely isolated from the infrastructure.

Feel free to look at the end result.

You would have no problem moving away from React and using another Front-End framework like Vue or Angular. If you did, you wouldn't need to modify any code that's part of the domain layer.

Not only could you use another Front-End framework, but you could even make this game a Back-End application!

Bonus: Data Fetching

At this point, you might feel like something is missing. In the real world, we need to manage states related to data fetching (isPending, etc.), as well as caching.

We cannot seriously rely on useEffect or useCallback, but need solutions that work at scale. For caching, you can usually do it directly at the repository level, or with a more advanced data fetching mechanism, inside your controller.

With React, a great library is React-Query from TanStack. Let's see how we can implement it.

After installation, we need to define the QueryClientProvider:

Then, we can use it in our controller, and treat our use case as a single request. For example with initialize, we could do:

In the same way, we could update the game controller with mutations for play and jumpTo: