Git Product home page Git Product logo

bespoyasov / frontend-clean-architecture Goto Github PK

View Code? Open in Web Editor NEW
2.3K 17.0 250.0 589 KB

React + TypeScript app built using the clean architecture principles in a more functional way.

Home Page: https://bespoyasov.me/blog/clean-architecture-on-frontend/

HTML 2.01% TypeScript 88.94% CSS 9.06%
clean-architecture onion-architecture domain application adapters layered-architecture functional

frontend-clean-architecture's Introduction

Other languages: Russian.

Frontend Clean Architecture

A React + TypeScript example app built using the clean architecture in a functional(-ish) way.

Things to Consider

There are a few compromises and simplifications in the code that are worth to be mentioned.

Shared Kernel

Shared Kernel is the code and data on which any modules can depend, but only if this dependency would not increase coupling. More details about the limitations and application are well described in the article "DDD, Hexagonal, Onion, Clean, CQRS, ... How I put it all together".

In this application, the shared kernel includes global type annotations that can be accessed anywhere in the app and by any module. Such types are collected in shared-kernel.d.ts.

Dependency in the Domain

The createOrder function uses the library-like function currentDatetime to specify the order creation date. This is not quite correct, because the domain should not depend on anything.

Ideally, the implementation of the Order type should accept all the necessary data, including the date, from outside. The creation of this entity would be in the application layer in orderProducts:

async function orderProducts(user: User, { products }: Cart) {
  const datetime = currentDatetime();
  const order = new Order(user, products, datetime);

  // ...
}

Use Case Testability

The order creation function orderProduct itself is framework-dependent right now and cannot be used or tested in isolation outside of React. The hook wrapper though is only used to provide the use case to components and to inject services into the use case itself.

In a canonical implementation, the function of the use case would be extracted outside the hook, and the services would be passed to the use case via a last argument or a DI:

type Dependencies = {
  notifier?: NotificationService;
  payment?: PaymentService;
  orderStorage?: OrderStorageService;
};

async function orderProducts(
  user: User,
  cart: Cart,
  dependencies: Dependencies = defaultDependencies
) {
  const { notifier, payment, orderStorage } = dependencies;

  // ...
}

Hook would then become an adapter:

function useOrderProducts() {
  const notifier = useNotifier();
  const payment = usePayment();
  const orderStorage = useOrdersStorage();

  return (user: User, cart: Cart) =>
    orderProducts(user, cart, {
      notifier,
      payment,
      orderStorage,
    });
}

In the sources, I thought it was unnecessary, as it would distract from the essence.

Crooked DI

In the application layer we inject services by hand:

export function useAuthenticate() {
  const storage: UserStorageService = useUserStorage();
  const auth: AuthenticationService = useAuth();

  // ...
}

In a good way, this should be automated and done through the dependency injection. But in the case of React and hooks, we can use them as a “container” that returns an implementation of the specified interface.

In this particular application, it didn't make much sense to set up the DI because it would distract from the main topic.

frontend-clean-architecture's People

Contributors

async10 avatar bespoyasov avatar fatso83 avatar shorthander avatar yeonuk-hwang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

frontend-clean-architecture's Issues

Does the application layer depend on the adapter layer?

import { useAuth } from "../services/authAdapter";
import { useUserStorage } from "../services/storageAdapter";

The above application layer authenticate code depends on the adapter layer's authAdatper and storageAdapter.

I noticed that you mentioned this place in your blog post:

In a canonical implementation, the use case function would be located outside the hook, and the services would be passed to the use case via the last argument or via a DI:

type Dependencies = {
  notifier?: NotificationService;
  payment?: PaymentService;
  orderStorage?: OrderStorageService;
};

async function orderProducts(
  user: User,
  cart: Cart,
  dependencies: Dependencies = defaultDependencies,
) {
  const { notifier, payment, orderStorage } = dependencies;
  // ...
}

The hook would then become an adapter:

function useOrderProducts() {
  const notifier = useNotifier();
  const payment = usePayment();
  const orderStorage = useOrdersStorage();

  return (user: User, cart: Cart) =>
    orderProducts(user, cart, {
      notifier,
      payment,
      orderStorage,
  });
}

If I understand correctly, the second piece of code will be placed in a file somewhere in the services or adapters folder, which if that is the case does meet the requirement that the application layer does not depend on the adapter layer.

The problem is that almost all use cases will have dependencies, which leads to the fact that all use cases under application will have an adapter in the adapter layer just to inject dependencies for that use case.

How to define Domain in practice?

Close to the database table (API) design? Close to the UI model design? Or should it be designed close to the business alone?

If it is close to the back-end API, it may be inconsistent with the data model needed for most front-end scenarios.

If it is close to the front-end UI model, it will become inconsistent when requirements change or when there are multiple clients.

If you design it completely by yourself according to the business requirements, first of all, the design cost and difficulty will go up, and there may be a small amount of the above two problems, i.e., it needs to be adapted to both the back-end API and the front-end UI model.

Improve UI

Hey pal. Great work done, I really love your code structure. I just want to contribute by improving the UI

Question about dependencies between domains and external libraries.

First of all, I wanted to thank you for the high-quality content; I have been learning a lot from you.

It's just a question of how I could improve, if necessary, on domain layer dependencies. In the case of creating an order, I have the part where I want to use some external library for the creation date or something similar, in this case, could I pass it as a parameter and have the use case provide me with this information? How could I do it?

And the other question is regarding dependencies between domains. In this case, could I also receive them as parameters, and the use case would give me this information?

image

Observation: I am Brazilian, and I used Google Translate for translation. I apologize if it is not clear.

Nextjs version

It will be awesome if we have frontend-clean-architecture for Next.js what do you think 😆

Isn't it superfluous to export functions in adapters instead of constants?

import { NotificationService } from "../application/ports";

export function useNotifier(): NotificationService {
  return {
    notify: (message: string) => window.alert(message),
  };
}

vs

import { NotificationService } from "../application/ports";

export const notifier: NotificationService = { 
  notify: (message: string) => window.alert(message) 
}

Calling functions that return a new service object each time leads to an overhead during code execution and also generates a bunch of garbage for GC. This is practically nothing - for the client, but if such code is used in SSR, this is already a problem

React Query implementation

Hello Alex, first off I would like to thank you for the detailed article and for sharing this with the community, I've played a bit with this architecture and now I want to implement it in a real world project, which is why I'm raising this issue, since I'm planning to use React Query as my server state management library and I can't figure out how or rather couldn't settle on a solution...

Since react query already handles state management and async calls I feel like it has two types of adapters in one (storage service and api service), i saw how you suggested to implement redux toolkit in one of the previous issues, but this one is different since i don't see how we can decouple react-query from the business logic here... unless we use the library directly with the UI layer (react) ? since it's a react specific library anyways unlike redux, but it must interact with the Application layer ofc for the reasons sepcified earlier... this where i'm being stuck now, since if there is a way to implement React query with the Clean Architecture, I think it would be a very solid combination.

Only potential solution I have now is to wrap each react query function (useQuery, useMutation etc...) in a hook and use it as an adapter somehow that implements a service and then use that in the application layer...

Thank you in advance.

Scaling this to the practical level - replacing store with big external state management library.

Hello, I see that implementation of storage is realized in useContext. Which is fine, that is just an implementation and nothing more.

But I am concerned that bringing some big external library like redux-toolkit would actually change architecture of your code a little bit, by changing your domain interfaces for storage.

How ?
Well redux brings actions, reducers, dispatching, and middlewares for async communication. And currently I can't see how can you completely separate all of this from your domain logic, and hide it behind your interfaces.

Also even if you manage to do it somehow ( like I did somehow.. somehow I mean it changed my domain interfaces but I managed to hide it again) it is still going to be against what is redux-toolkit paradigm, and you will end up with single reducer that sets whole slice.. I am not sure if that's okay?

Here is what I managed to do and all my concerns in one place : https://stackoverflow.com/questions/71649550/clean-architecture-react

I hope you will find the time to answer this.
Thanks for your time.

Is the domain level independent of the UI layer?

First of all, thanks for the interesting example, Alex.

But... I'd like to ask one question.

I've seen many attempts to implement clean architecture when working on web interfaces, but actually, I face the same problem every time.

The specific of the web development is that we want to update UI pointwise, instead of re-rendering the whole page. And usually, we use tools that allow us to do it effectively. I mean React, Redux, and MobX are the tools that allow us to do local UI updates, right? But we pay for that by using the API provided by these tools.

Let me clarify what I mean on the example of your application.

Why do you create a new cart object with a new product array here instead of mutating this? It's because React forces you to do this, isn't it? If you don't create a new copy, React will not re-render your Cart component. But it means that the logic inside the domain layer depends on the UI layer. If you decide to replace React with VueJS, or for example if you decide just to add MobX to your React application, approaches in the domain namespace will not work anymore or at least, they will not work effectively.

That's why from my point of view, clean architecture actually is not as perfect for the front-end apps as on paper.

What do you think about this?

Isn't it redundant to store data in the domain and in the storage at the same time?

Why not transfer the data storage/retrieval functionality to the storage? The functionality for checking referral data integrity could also be taken out there.

This question applies in particular to state managers - is it worth duplicating their functionality in domain? After all, state managers are very closely related to the domain and therefore must be aware of referral integrity and other constraints in the data schema.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.