This article is an English translation of the original in my blog: Alejándonos de ReactJs y VueJs en el front end usando Clean Architecture.
One of the advantages of using Clean Architecture, among others, is the ability to uncouple our application of the delivery mechanism to the user, that is, from the UI framework or library.
This advantage in long-term applications allows us to adapt in the future to the changes that will surely take place in libraries and frameworks.
In this article, we will take Clean Architecture to the extreme in the front-end by applying two delivery mechanisms: ReactJS and VueJs.
We will have as much code as possible reused between the two implementations.
This will be possible by creating the domain, data, and remote display logic of ReactJs and VueJs.
Why move away from the framework?
I have developed different technologies applying Clean Architecture like .Net, Android, iOS, and Flutter. For a long time, I am also programming in the front-end and writing about it.
One of the biggest problems when it comes to evolving an application is the coupling to the UI framework.
On the front-end little by little due to the responsibilities that applications of this type have been gained over time, it makes more and more sense to develop in a more structured way and the problems to be solved are very similar to those that exist on other fronts such as backend or mobile development.
There are frameworks like ReactJs and VueJs that make life easier for us to take on these challenges on the front-end.
A front-end application today is an independent application of the backend in many cases and therefore needs to have its own architecture.
In addition, this architecture must help us in the next points:
- Independent of UI, frameworks, API rest and persistence, databases o third-party services.
- Escalability.
- Testability.
This means that if we change the vision of having a ReactJs or VueJs application, to have a front-end application that uses ReactJs or VueJs to render, this will make our lives much easier in the future.
So, for example, evolving your ReactJS application from using classes as before, to using functions and hooks as is done now, is much more trivial. The same happens if you switch in VueJS from using the options API to the composition API.
It’s more trivial because you only use the framework for what is strictly necessary, such as rendering and thus you do not overuse it, keeping it away from any type of logic, be its domain, data, or presentation logic.
Frameworks evolve and you cannot control that, but what you can control is the coupling you have with them and how their changes affect you.
But in this case, we are going to go beyond how to adapt to changes that can happen in a framework and we are going to see the amount of code that could not change when we modify ReactJS by VueJS if we use Clean Architecture and separate responsibilities.
This is the picture you keep in mind if you develop using Clean Architecture.
If you don’t have clear the concepts of Clean Architecture, I recommend that you read this article.
The most important part is the dependency rule, so if you don’t know what I’m talking about, I recommend that you read this article.
The example that we are going to see is based on the one we saw in this article.
Our scenario
It’s a shopping cart with enough functionality to look like a real example. We are going to have a global state, non-global state, and we will simulate invoke to a remote service.
Architecture
At the project structure level, we will use a monorepo using yarn workspaces, in this way we can split the project into modules or packages sharing code between them.
We have several packages:
- Core: in this package, we will have all the shared code between the app rendered by ReactJS and the app rendered by VueJs.
- React: in this package is found the react app version.
- Vue: in this package is found the Vue app version.
¿What code is reused?
We are going to reuse all the code that we must have uncoupled from the UI Framework, since being different versions of the same app it makes sense that this code is shared and not write twice.
This is a demonstration exercise of the potential that Clean Architecture has but this uncoupling of the UI framework is necessary even when we develop a real app.
Using the UI framework for what is strictly necessary allows us to better adapt to changes in future versions of the framework.
This is because the code that contains the application logic, which is the most important part, that changes less over time, and is the code potentially to be shared between two versions of the same app as in this example, it’s uncoupled without depending on the UI framework.
In Clean Architecture the domain layer is where the enterprise and application business logic is located.
The data layer is where we communicate with the persistence.
The presentation logic is the one that decides what data is shown if something should be visible or not if it should be shown to the user that we are loading data or if an error should be displayed. It is where the state of the components is managed.
Each of these 3 parts contains logic that we must uncouple and is found in the core package.
Domain Layer
The domain layer is where the enterprise and application business logic is located.
Use Cases
Use cases are intents, contains the business logic of the application, they are actions and in this example, we have the next:
- GetProductsUseCase
- GetCartUseCase
- AddProductToCartUseCase
- EditQuantityOfCartItemUseCase
- RemoveItemFromCartUseCase
Let’s see the example of GetProductsUseCase:
export class GetProductsUseCase {
private productRepository: ProductRepository;
constructor(productRepository: ProductRepository) {
this.productRepository = productRepository;
}
execute(filter: string): Promise<Either<DataError, Product[]>> {
return this.productRepository.get(filter);
}
}
This use case is simple because it consists of a simple call to the data layer, in other contexts where, for example, when creating a product, we have to validate that there is no longer one with the same SKU, there would be more logic.
The use cases returns Either type, if you are not sure what it is then I recommend that you read this article and this article.
In this way, the error handling is not done using the catch of the promises, but the result object of the promise itself tells you if the result is successful or not.
The use of Either versus the classic try-catch has several advantages:
- The flow of execution is simpler to follow without jumps between callers when an error occurs.
- That something can go wrong, is explicitly indicated. Errors that may occur are explicitly indicated.
- Doing the use of the exhaustive switch, if you add more errors in the future, TypeScript will warn you where you have not taken this new error into account.
The type for the errors is as follows:
export interface UnexpectedError {
kind: "UnexpectedError";
message: Error;
}
export type DataError = UnexpectedError;
Potentially in the future, it could evolve to something like this:
export interface ApiError {
kind: "ApiError";
error: string;
statusCode: number;
message: string;
}
export interface UnexpectedError {
kind: "UnexpectedError";
message: Error;
}
export interface Unauthorized {
kind: "Unauthorized";
}
export interface NotFound {
kind: "NotFound";
}
export type DataError = ApiError | UnexpectedError | Unauthorized;
And in the presentation layer, if I’m using an exhaustive switch, Typescript would warn me, I should add more cases for each new error.
Entities
The entities contain the enterprise business logic.
Let’s see the example of Cart:
type TotalPrice = number;
type TotalItems = number;
export class Cart {
items: readonly CartItem[];
readonly totalPrice: TotalPrice;
readonly totalItems: TotalItems;
constructor(items: CartItem[]) {
this.items = items;
this.totalPrice = this.calculateTotalPrice(items);
this.totalItems = this.calculateTotalItems(items);
}
static createEmpty(): Cart {
return new Cart([]);
}
addItem(item: CartItem): Cart {
const existedItem = this.items.find(i => i.id === item.id);
if (existedItem) {
const newItems = this.items.map(oldItem => {
if (oldItem.id === item.id) {
return { ...oldItem, quantity: oldItem.quantity + item.quantity };
} else {
return oldItem;
}
});
return new Cart(newItems);
} else {
const newItems = [...this.items, item];
return new Cart(newItems);
}
}
removeItem(itemId: string): Cart {
const newItems = this.items.filter(i => i.id !== itemId);
return new Cart(newItems);
}
editItem(itemId: string, quantity: number): Cart {
const newItems = this.items.map(oldItem => {
if (oldItem.id === itemId) {
return { ...oldItem, quantity: quantity };
} else {
return oldItem;
}
});
return new Cart(newItems);
}
private calculateTotalPrice(items: CartItem[]): TotalPrice {
return +items
.reduce((accumulator, item) => accumulator + item.quantity * item.price, 0)
.toFixed(2);
}
private calculateTotalItems(items: CartItem[]): TotalItems {
return +items.reduce((accumulator, item) => accumulator + item.quantity, 0);
}
}
In this example, the entities are simple, with properties of primitive types, but a real example where there were validations we could have Entities and Value Objects defined as classes and with factory methods where the validation is performed. We use Either to return the errors or the result.
Boundaries
The boundaries are the abstractions of the adapters, for example, in Hexagonal Architecture they are called ports. They are defined in the layer of the use cases in the domain and indicate how we are going to communicate with the adapters.
For example, to communicate with the data layer we use the repository pattern.
export interface ProductRepository {
get(filter: string): Promise<Either<DataError, Product[]>>;
}
Data Layer
The data layer is where the adapters are found and an adapter is responsible to transform the information between the domain and external systems.
External systems may be a web service, a database, etc…
In this simple example, I’m using the same entities that represent the product, shopping cart, and cart items between the presentation, domain, and data layers.
In real applications, is common to have a different data structure for each layer or even to have Data Transfer Objects (DTOs) to pass data between layers.
In this example, we have repositories that return data stored in memory.
const products = [
...
];
export class ProductInMemoryRepository implements ProductRepository {
get(filter: string): Promise<Either<DataError, Product[]>> {
return new Promise((resolve, _reject) => {
setTimeout(() => {
try {
if (filter) {
const filteredProducts = products.filter((p: Product) => {
return p.title.toLowerCase().includes(filter.toLowerCase());
});
resolve(Either.right(filteredProducts));
} else {
resolve(Either.right(products));
}
} catch (error) {
resolve(Either.left(error));
}
}, 100);
});
}
}
The important thing is to understand that the repository is an adapter and that its abstraction or port is defined in the domain, so the traditional direction of the dependency is inverted.
This is the most important part of Clean Architecture, the domain should not have any dependency on external layers, in this way it is uncoupled and it will be easier to replace an adapter with another in the future or even for testing purposes.
In this way, if we replace the adapter implementation with one that invokes a web service, the domain is not affected and therefore we are hiding implementation details.
Presentation Layer — Adapters
The adapters of the presentation layer are the last reuse part of our core package and It’s where we hook the UI React or Vue layers.
These adapters are also reusable between the two versions of the app, they are intermediaries between the UI components and the domain layer.
They contain the presentation logic, deciding what information is shown, what should be visible, etc…
The state management is performed by this layer and does not depend on React or Vue.
There are different presentation patterns that we can use. In this case, I am using the BLoC Pattern because it fits very well with declarative frameworks such as React and Vue.
If you want to delve into the BLoC pattern, I recommend that you read this article.
As I discussed in that article, when you use BLoC with Clean Architecture, it makes more sense to call them PLoC, Presentation Logic Component. So in this example, they are named this way.
Let’s see the shopping cart example:
export class CartPloc extends Ploc<CartState> {
constructor(
private getCartUseCase: GetCartUseCase,
private addProductToCartUseCase: AddProductToCartUseCase,
private removeItemFromCartUseCase: RemoveItemFromCartUseCase,
private editQuantityOfCartItemUseCase: EditQuantityOfCartItemUseCase
) {
super(cartInitialState);
this.loadCart();
}
closeCart() {
this.changeState({ ...this.state, open: false });
}
openCart() {
this.changeState({ ...this.state, open: true });
}
removeCartItem(item: CartItemState) {
this.removeItemFromCartUseCase
.execute(item.id)
.then(cart => this.changeState(this.mapToUpdatedState(cart)));
}
editQuantityCartItem(item: CartItemState, quantity: number) {
this.editQuantityOfCartItemUseCase
.execute(item.id, quantity)
.then(cart => this.changeState(this.mapToUpdatedState(cart)));
}
addProductToCart(product: Product) {
this.addProductToCartUseCase
.execute(product)
.then(cart => this.changeState(this.mapToUpdatedState(cart)));
}
private loadCart() {
this.getCartUseCase
.execute()
.then(cart => this.changeState(this.mapToUpdatedState(cart)))
.catch(() =>
this.changeState({
kind: "ErrorCartState",
error: "An error has ocurred loading products",
open: this.state.open,
})
);
}
mapToUpdatedState(cart: Cart): CartState {
const formatOptions = { style: "currency", currency: "EUR" };
return {
kind: "UpdatedCartState",
open: this.state.open,
totalItems: cart.totalItems,
totalPrice: cart.totalPrice.toLocaleString("es-ES", formatOptions),
items: cart.items.map(cartItem => {
return {
id: cartItem.id,
image: cartItem.image,
title: cartItem.title,
price: cartItem.price.toLocaleString("es-ES", formatOptions),
quantity: cartItem.quantity,
};
}),
};
}
}
The base class of all PLoCs is responsible for storing the state and notifying when it changes.
type Subscription<S> = (state: S) => void;
export abstract class Ploc<S> {
private internalState: S;
private listeners: Subscription<S>[] = [];
constructor(initalState: S) {
this.internalState = initalState;
}
public get state(): S {
return this.internalState;
}
changeState(state: S) {
this.internalState = state;
if (this.listeners.length > 0) {
this.listeners.forEach(listener => listener(this.state));
}
}
subscribe(listener: Subscription<S>) {
this.listeners.push(listener);
}
unsubscribe(listener: Subscription<S>) {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
}
}
All the information that the UI component needs must be interpreted from the state, elements to render in a table or list, but also if something should be visible or not, such as the shopping cart, the loading, or an error to show.
export interface CommonCartState {
open: boolean;
}
export interface LoadingCartState {
kind: "LoadingCartState";
}
export interface UpdatedCartState {
kind: "UpdatedCartState";
items: Array<CartItemState>;
totalPrice: string;
totalItems: number;
}
export interface ErrorCartState {
kind: "ErrorCartState";
error: string;
}
export type CartState = (LoadingCartState | UpdatedCartState | ErrorCartState) & CommonCartState;
export interface CartItemState {
id: string;
image: string;
title: string;
price: string;
quantity: number;
}
export const cartInitialState: CartState = {
kind: "LoadingCartState",
open: false,
};
In this case through union types of typescript, we can more securely and functionally model our state using sum algebraic data types.
This way of modeling is less prone to errors because you indicate that a very clear form that the state has 3 main possibilities:
- Loading information
- An error has occurred
- Updated data
Presentation Layer — UI
In this layer is where are the components and everything related to React or Vue such as components, hooks, applications, etc.
The components are very simple and light because they are free to manage any type of logic or state management, this is the responsibility of each of the layers in the core package.
React App
In react we will have the components that render our list of products, the app bar with the number of products in the cart, and the product cart rendered as a Sidebar.
Let’s see the example of the component that renders the content of the cart.
import React from "react";
import { makeStyles, Theme } from "@material-ui/core/styles";
import { List, Divider, Box, Typography, CircularProgress } from "@material-ui/core";
import CartContentItem from "./CartContentItem";
import { CartItemState } from "@frontend-clean-architecture/core";
import { useCartPloc } from "../app/App";
import { usePlocState } from "../common/usePlocState";
const useStyles = makeStyles((theme: Theme) => ({
totalPriceContainer: {
display: "flex",
alignItems: "center",
padding: theme.spacing(1, 0),
justifyContent: "space-around",
},
itemsContainer: {
display: "flex",
alignItems: "center",
padding: theme.spacing(1, 0),
justifyContent: "space-around",
minHeight: 150,
},
itemsList: {
overflow: "scroll",
},
infoContainer: {
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100vh",
},
}));
const CartContent: React.FC = () => {
const classes = useStyles();
const ploc = useCartPloc();
const state = usePlocState(ploc);
const cartItems = (items: CartItemState[]) => (
<List className={classes.itemsList}>
{items.map((item, index) => (
<CartContentItem key={index} cartItem={item} />
))}
</List>
);
const emptyCartItems = () => (
<React.Fragment>
<Typography variant="h6" component="h2">
Empty Cart :(
</Typography>
</React.Fragment>
);
switch (state.kind) {
case "LoadingCartState": {
return (
<div className={classes.infoContainer}>
<CircularProgress />
</div>
);
}
case "ErrorCartState": {
return (
<div className={classes.infoContainer}>
<Typography display="inline" variant="h5" component="h2">
{state.error}
</Typography>
</div>
);
}
case "UpdatedCartState": {
return (
<React.Fragment>
<Box flexDirection="column" className={classes.itemsContainer}>
{state.items.length > 0 ? cartItems(state.items) : emptyCartItems()}
</Box>
<Divider />
<Box flexDirection="row" className={classes.totalPriceContainer}>
<Typography variant="h6" component="h2">
Total Price
</Typography>
<Typography variant="h6" component="h2">
{state.totalPrice}
</Typography>
</Box>
</React.Fragment>
);
}
}
};
export default CartContent;
Hooks
Using Clean Architecture, hooks are not used? Yes, they are used, but for what is strictly necessary.
The state will not be managed with hooks, the side effects are not triggered from hooks, this is the responsibility of the PloCs in the core package.
But we will use them to store the final state of the component that its PloC returns to us and we will use them to share context between components or react to the change of state that the PloC returns to us.
Let’s see how the usePLocState hook that we used in the component is defined:
export function usePlocState<S>(ploc: Ploc<S>) {
const [state, setState] = useState(ploc.state);
useEffect(() => {
const stateSubscription = (state: S) => {
setState(state);
};
ploc.subscribe(stateSubscription);
return () => ploc.unsubscribe(stateSubscription);
}, [ploc]);
return state;
}
This custom hook is in charge of subscribing to the PloC state changes and storing the final state.
Vue App
In Vue, we will also have the same components as in the React version.
Now let’s see the component that renders the content of the shopping cart in the Vue version:
<template>
<div id="info-container" v-if="state.kind === 'LoadingCartState'">
<ProgressSpinner />
</div>
<div id="info-container" v-if="state.kind === 'ErrorCartState'">Error</div>
<div id="items-container" v-if="state.kind === 'UpdatedCartState'">
<div v-if="state.items.length > 0" style="overflow: scroll">
<div v-for="item in state.items" v-bind:key="item.id">
<CartContenttItem v-bind="item" />
</div>
</div>
<h2 v-if="state.items.length === 0">Empty Cart :(</h2>
</div>
<Divider />
<div id="total-price-container">
<h3>Total Price</h3>
<h3>{{ state.totalPrice }}</h3>
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from "vue";
import { CartPloc } from "@frontend-clean-architecture/core";
import { usePlocState } from "../common/usePlocState";
import CartContenttItem from "./CartContenttItem.vue";
export default defineComponent({
components: {
CartContenttItem,
},
setup() {
const ploc = inject<CartPloc>("cartPloc") as CartPloc;
const state = usePlocState(ploc);
return { state };
},
});
</script>
<style scoped>
#info-container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
#items-container {
display: flex;
flex-direction: column;
align-items: center;
min-height: 150px;
justify-content: space-around;
}
#total-price-container {
display: flex;
align-items: center;
padding: 8px 0px;
justify-content: space-around;
}
</style>
As you can see, it looks a lot like the React version using composition API.
Composition API
In the Vue version we will also have hooks, such as the one that manages the subscription to changes in the PLoC state:
import { Ploc } from "@frontend-clean-architecture/core";
import { DeepReadonly, onMounted, onUnmounted, readonly, Ref, ref } from "vue";
export function usePlocState<S>(ploc: Ploc<S>): DeepReadonly<Ref<S>> {
const state = ref(ploc.state) as Ref<S>;
const stateSubscription = (newState: S) => {
state.value = newState;
};
onMounted(() => {
ploc.subscribe(stateSubscription);
});
onUnmounted(() => {
ploc.unsubscribe(stateSubscription);
});
return readonly(state);
}
Dependency Injection
From the React and Vue app, we have to create or reuse the PloC structure for each component: use cases and repositories.
If these concepts were defined in the core package, the part responsible for creating them may be in the core package as well.
This time I am using the Service Locator pattern statically:
function provideProductsPloc(): ProductsPloc {
const productRepository = new ProductInMemoryRepository();
const getProductsUseCase = new GetProductsUseCase(productRepository);
const productsPloc = new ProductsPloc(getProductsUseCase);
return productsPloc;
}
function provideCartPloc(): CartPloc {
const cartRepository = new CartInMemoryRepository();
const getCartUseCase = new GetCartUseCase(cartRepository);
const addProductToCartUseCase = new AddProductToCartUseCase(cartRepository);
const removeItemFromCartUseCase = new RemoveItemFromCartUseCase(cartRepository);
const editQuantityOfCartItemUseCase = new EditQuantityOfCartItemUseCase(cartRepository);
const cartPloc = new CartPloc(
getCartUseCase,
addProductToCartUseCase,
removeItemFromCartUseCase,
editQuantityOfCartItemUseCase
);
return cartPloc;
}
export const dependenciesLocator = {
provideProductsPloc,
provideCartPloc,
};
We could also use a dynamic Service Locator together with Composition Root or a dependency injection library.
In the React app, there is a global state that must be shared, it is the shopping cart. Therefore CartPloc, which is the one who manages this state, must be shared and accessible by all components.
React
In React we solve this using createContext and a custom hook using useContext.
export function createContext<T>() {
const context = React.createContext<T | undefined>(undefined);
function useContext() {
const ctx = React.useContext(context);
if (!ctx) throw new Error("context must be inside a Provider with a value");
return ctx;
}
return [context, useContext] as const;
}
const [blocContext, usePloc] = createContext<CartPloc>();
export const useCartPloc = usePloc;
const App: React.FC = () => {
return (
<blocContext.Provider value={dependenciesLocator.provideCartPloc()}>
<MyAppBar />
<ProductList />
<CartDrawer />
</blocContext.Provider>
);
};
export default App;
Using the custom useCartPloc we have access from any component to this PloC and its state.
Vue App
In Vue, we solve this by using the provide feature.
<template>
<div id="app">
<MyAppBar />
<ProductList searchTerm="Element" />
<CartSidebar />
</div>
</template>
<script lang="ts">
import { dependenciesLocator } from "@frontend-clean-architecture/core";
import { defineComponent } from "vue";
import MyAppBar from "./appbar/MyAppBar.vue";
import ProductList from "./products/ProductList.vue";
import CartSidebar from "./cart/CartSidebar.vue";
export default defineComponent({
name: "App",
components: {
ProductList,
MyAppBar,
CartSidebar,
},
provide: {
cartPloc: dependenciesLocator.provideCartPloc(),
},
});
</script>
Later from any component, we have access to the PLoC and its state using:
const cartPloc = inject <CartPloc> (“cartPloc”) as CartPloc;
Source code
The source code can be found here: frontend-clean-architecture.
Related Articles And Resources
- Spanish newsletter: https://xurxodev.com/#/portal/signup
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design
- Clean Architecture Course.
- ¿Wy use I Clean Architecture in my projects?
- The Bloc Pattern in Clean Architecture
- The BLoC Pattern in Clean Architecture in ReactJS
- The BLoC Pattern in Clean Architecture in Flutter
- Clean Architecture: Code Smells. Parte 1
- Clean Architecture: Code Smells. Parte 2
- My surreal purchase of the book Clean Architecture
Conclusions
In this article, we have seen a Clean Architecture implementation on the front-end.
We have a version of React and Vue app reusing as much code as possible between the two and placing it in a core package.
With this exercise of having a core package with all the logic uncoupled from the framework, we can appreciate the power that Clean Architecture can offer us on the front-end.
Organizing the project as a monorepo and having a core package has been necessary for this example, but it is not necessary when developing an app either React or Vue.
However, it is an interesting exercise to force you to uncouple from the UI framework as it can sometimes be difficult to see that you are coupling, especially at the beginning.
Top comments (84)
The article title seems a little clickbaity for my taste. Even the Spanish one, same thing. From the title, you gather that the article is about NOT using VueJS/React on the front-end .
That’s what tricked me into reading this article. 🤷🏻♀️
Same ☝️.
I'm so glad I was "click-baited" into reading this article. This article, is one of the best frontend architecture articles I've read in a long time.
On Using Clean Architecture to Decouple Client Side Architecture from React.js or Vue.js
Similarly: A different approach to frontend architecture
That's true. When I read the title, I thought it was about not using VueJS or ReactJS.
I disagree since "moving away from" is different from "moving away from using".
same, also clicked because of the title. just seems like a translation error though, i don't think the author had any bad intentions.
i searched for it .. love you for such a beautiful article . gonna skim through your whole blog today :)
+1
I'm joining a meeting and could read few parts so I may come back later but now and just to clarify Angular is a Framework, also it is Vue, but not React or Preact which are Libs, there are so many differences (from the engineering point of view) that is not even ok to try to compare them 😅
A Framework makes you work on the way this framework was conceived while a lib let you choose which parts to use and how, how to structure your project and so on (that's also the reason why React is the TOP1 in loved, used and demanded)
Well if you think about it React forces you to write applications in a determined way too.
It's just less opinionated on some topics but still gives you a way to do things, the React way. Just saying the thing you have to pass props, never change props, and component will re-render automatically on changed props(or state) without having to manually tell the component what changed, that's already enough to enforce you a way to develop.
Just because React is less opinionated on things like global storage, router, i18n, forms etc.. It doesn't mean it's not a framework
But it's not, and it's not my opinion. React and Preact are not framewors and it's a fact. It's like saying Node is a framework or a language when Node is only a javascript runtime environment so in order to speak consistently and concisely, we need to categorise and qualify things like they are.
In computer programming, a software framework is an abstraction in which software, providing generic functionality, can be selectively changed by additional user-written code, thus providing application-specific software. It provides a standard way to build and deploy applications and is a universal, reusable software environment that provides particular functionality as part of a larger software platform to facilitate the development of software applications, products and solutions. Software frameworks may include support programs, compilers, code libraries, toolsets, and application programming interfaces (APIs) that bring together all the different components to enable development of a project or system.
Frameworks have key distinguishing features that separate them from normal libraries:
By the other hand, a software library is a suite of data and programming code that is used to develop software programs and applications. It is designed to assist both the programmer and the programming language compiler in building and executing software. It generally consists of pre-written code, classes, procedures, scripts, configuration data and more. Typically, a developer might manually add a software library to a program to achieve more functionality or to automate a process without writing code for it.
That being said, from the different options we have for the front-end, i'll pick 5 and they are categorised like this:
Hope it helps you understand the differences :)
Answering from non-work account
That's a really good answer. Yes even tho React has some sort of DI you can't really say it's extensible
When you import a lib just to achieve a goal you usually fit into this approach, otherwise you can either choose another one (and fit into it) or write down your approach and publish it as the new lib to do the same others do but on a different manner, then your lib will need to pass 2 filters:
I can understand your reasons to avoid using a lib or framework "as is" and in the end you can agree with me that is a try to avoid tech debt in a project, not less not more.
Then there reasons that counters this reinvented wheel. If you use react millions of devs out there will be available to work within your project in few hours, if you need higher performance and less included libs, you can simply pick preact and every react dev will be skilled to work with that as well.
If you create your own approach and build a project or even a company with that, the end result is that the world keeps rolling and new tech will appear anyway to solve other problems you don't even know about -yet- or to automate things you didn't even mind -yet- so people will like to use this "latest tech" (We know "latest" mean that it appeared 4-5 or 7 years ago and now it's popular and everyone works with that) so even if you figure out how to trick some dev to work in your company or project, they will eventually fly away to another company at the first sight of working on a non standard dinosaur. This will put you in need to increase the salary for your devs and new hires which is also costly and at this point, the life-cycle of your App will be near the end and needs a tech refresh regardless of your initial effort.
The end questions would be then, Is it worth? what do you prefer?
Hi, thanks for your comment.
Everything has advantages and disadvantages but in long-term applications I think it makes more sense to uncouple and in the future it will be appreciated in my experience.
Regards
Bet you don't pay the salaries so! 😁
It's not worth to argue if it's a framework or library? There's no clear boundary. This is also not the main point of this article. This is article is about decoupling your core logic from third party code. No matter you call it a framework or library, so that in case you have to migrate to a different library or framework, you don't have to rewrite all your core logic. For the front end world, it's more important as it evolves so fast. Don't expect something you love today will survive for long time.
Please, I know we live in an opinionated world where people believe what see in tik-tok and so but IT is a science, thus is well categorised. Before being ignorant in front of everyone and especially after the previous explanation make the simple search "react" in Google and you'll see:
React – A JavaScript library for building user interfaces.
Then search "Angular" and you will be able to read the following:
"Angular is a TypeScript-based free and open-source web application framework led by the Angular Team at Google..."
if your opinion is that React is a framework you must try to reach React devs and ask them to change the category of it's Library.
Maybe they can explain what I tried before on a way you can understand it.
So NO, you cannot use React as a framework because it's not a framework, the same way you cannot use Angular as library because it's not a library.
You cannot use a car to travel from europe to america because it's not a plane the same way you cannot use a plane for your commitment to work because it's not a car, even they are both vehicles.
It's a really nice article. However, I'd like to have some complaints/suggestions:
I could not more disagree on spacing. Although it's always a personal preference - I prefer 4-chars spacing, as it makes the code much better readable
To the people getting triggered because of the "framework vs lib" discussion. Plus some considerations in regards of the article ;)
Both (React and Vuejs) are considered a "Delivery Mechanism" from the clean architecture point of view, so following that idea both fall into the same category, no matter if one is a framework and the other a library. Both are an abstraction on top of the DOM to facilitate building user interfaces. That's one of the important parts of the article together with trying to decouple/extracting important logic from it, so your code doesn't depend on any lib/framework that much. So, discussing if it's a framework or not is a bit pointless in my opinion.
I think one point to discuss is, if taking the time to code like this makes sense ? or at least to this high degree of decoupling. Because part of the idea is that you could easily interchange the delivery mechanism if you needed to. But in real life, how often does this happens? Which company/startup would be successful enough to last that many years where you would be in need of using a different lib/framework?
Don't get me wrong, this approach makes you write more testable code which is always a good thing and definitely is an approach that should/could be used when developing back-end systems, and personally I like it and try to stick to it as much as possible (it will always depend on the team though).
basically any company that's survived not fully digitizing until now - which is a large part of enterprise IT, manufacturing, banks and insurances. Startups are only a fraction of the market for User Interfaces. UIs can go into products facing customers or facing internal users. Internal promoters of a digitization project will easily get their budget turned down if the UI can't survive for a couple of years at least, and that means salvaging as much as possible from past projects
If you read all the comments about you'll probably find the point on the discussion avobe but I'll expand my boundaries to this one :D
I agree with you that being in need to change the lib/fw for the frontend would take years or decades. I.e. Spring that was released in 2002, Ruby on Rails in 2004, Symfony in 2005 and they are still being used widely. But let's think we've an Angular2 framework and that Google stop maintaining it like they did with tones of tech they released in the past ;)
In the matter of discussion we can introduce a workaround used for migrating monoliths into services (front end side) that also fits well for migrating modern frontend stacks.
You simply pick a feature, isolate it, build a new JS app with that -stand alone- and embed it into any element of your current view where you want to show that and connect it to the global state (if any).
Now you have an entire Angular app but a feature is provided with a different new hypothetical lib.
This is usually a bit easier to handle if you've a lib than if you've a framework and by the other hand the major part of logic on the frontend is related to conditional rendering of elements and having a 1st layer of security in the shape of form validation, which is easy to understand, document and replicate elsewhere. On top of that there's the fact of having functionalities write down in JS and picking an hypothetical JS lib for the "new frontend", then you'll be able to simply copy-paste your preferred methods/functions and refactor them with the "current future ES version" if you want to (js is retro compatible so it may not be even a need).
This is also easy with a lib where you call methods and just the ones that this lib provide than migrating a ng-if statement because it will need to be rewritten according to the new framework or lib.
At the end you'll have a good modern "new" application with the current stack, methodologies etc.... part by part, feature by feature, view by view... whatever is better depending on your base architecture (usually with the current stack would probably be component by component).
So it's this kind of decoupling really needed? Isn't it a patch to make the job of the devs more difficult to get in return... almost nothing? Wouldn't be better to design a good architecture from the beginning to make this process described above as easier as possible and nothing more?
Love it. So rare to see somebody discuss this beyond beginner/intermediate level topics like architecture with frontend UI!
Where were you 3 years ago?! ;)
Thanks!
This is an awesome article to me although the title made me confused a little bit at first. Honestly, I really want to learn more from your mentioned links in it, but they're all in Spanish. I think it would be a great help to the readers who want to learn more about your article if you could help to translate them to English. I'm willing to read them as soon as possible I know them translated.
Once again, thank you for the great job!
Okay, i've tried to think of ways to say this without coming off as harsh. I'm going to give pointed criticism and i promise my intent is not to "bash" you. I'm just going to give my honest take.
The Code
The code is bad, doesn't take advantage of implicit returns, hastily abstracts dependencies, makes decent integration testing fairly cumbersome because of the amount of dependency mocking you'd need to do. All in all, it would literally not pass code review, switch statements in a render function vs a hash map? This would get you paired with a senior for a few hours to fix this mess, it's that bad. It's a huge red flag. Too many code smells.
The Concept
The concept is a reasonable one for the backend where the "state" is usually colocated and composability isn't favored over inheritence. But it's just not great for the frontend. It's not composable, it's not easily maintained, it's not extensible, it's tightly coupled to your dependency setup, it's difficult to read, it's difficult to reason about, it's hard to test and therefore it should literally be avoided like the plague. It might actually be one of the worst architectures i've seen for frontend frameworks, including Anguler 1 (insider joke).
If your component/class isn't readable and you cannot discern what it's attempting to do in a short period of time, or extend it with confidence, in the tests/implementation, that it wouldn't break, it's not the solution to the problem.
This is the hastiest of hasty abstractions. Please never write code like this for the frontend. It's fun to see, but it's definitely not realistic or production worthy.
If this comes off as mean spirited, i promise that's not my intent, i'm trying to approach this in good faith.
Eventually, we are going to be moving away from React and Vue, so the question is how much of your codebase you can throw away then. Anyone remember flash-games, Rich Internet Applications and Silverlight?... 👵🧙
For the business logic part it's clear why you seperate it into the domain, what do you think about separating the UI/UX logic into its own domain as well? I've worked on collecting the most common UI phrases, properties and classes with some documentation and assigning a URL to them. The domains of designers, copy and product owners are more in the daily work of a frontend developer. So for the dev, it's going to be hard to advocate for an architecture that coworkers don't benefit from right then and there.