Usando custom hooks como adaptador de presentación
Hace tiempo escribí sobre el Patrón BloC como alternativa a otros patrones de presentación como MVP, MVVM usándolo como adaptador de presentación.
Vimos ejemplos en diferentes tecnologías declarativas como React, Vue y Flutter porque encaja muy bien.
Hace un año escribí un artículo donde explicaba como aplicar el Patrón Bloc en fontend junto a clean Architecture, de forma que esto nos podía permitir reutilizar mucho código entre una aplicación React y su misma versión en Vue.
Como vimos en el artículo, esta estrategía no solo nos permitía compartir el dominio entre las dos tecnologías sino también la gestión de estado y lógica de presentación.
En esta ocasión vamos a ver como podemos utilizar custom hooks en React como adaptador de presentación para gestionar el estado y la lógica de presentación cuando utilizamos Clean Architecture en lugar del Patrón BloC, así como las ventajas e inconvenientes de esta estrategía.
Nuestro escenario
El escenario va a ser el mismo que utilizamos en el artículo Alejándonos de ReactJs y VueJs en frontend usando Clean Architecture. Si no te lo habías leído, te recomiendo que lo leas para tener contexto.
Ejemplo con PloC
Recordad que me gusta más al patrón BLoC llamarlo PLoC (Presentation Logic component).
En el ejemplo de CartPloc, el PloC es el encargado de la gestión de estado y la lógica de presentación.
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,
};
}),
};
}
}
A través del Ploc base se permite suscribirse a cambios de estado.
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);
}
}
}
Utilizabamos un custom hook genérico y reutilizable desde cualquier componente para suscribirse a los cambios de estado en el PloC.
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;
}
Ejemplo con custom hooks
En el ejemplo con custom hooks, estos reemplazan completamente al PloC para gestionar la lógica de presentacion y la gestión del estado.
Van a usar hooks típicos de react como useState, useEffect, useMemo.
export function useCart(
private getCartUseCase: GetCartUseCase,
private addProductToCartUseCase: AddProductToCartUseCase,
private removeItemFromCartUseCase: RemoveItemFromCartUseCase,
private editQuantityOfCartItemUseCase: EditQuantityOfCartItemUseCase) {
// Cart as Global state
const [state, setState] = useCartContext();
useEffect(() => {
getCartUseCase
.execute()
.then(cart => setState(this.mapToUpdatedState(cart)))
.catch(() =>
setState({
kind: "ErrorCartState",
error: "An error has ocurred loading products",
open: this.state.open,
})
);
}, [getCartUseCase]);
const closeCart = () => {
setState({ ...this.state, open: false });
}
const openCart = () => {
setState({ ...this.state, open: false });
}
const removeCartItem = () => {
removeItemFromCartUseCase
.execute(item.id)
.then(cart => setState(this.mapToUpdatedState(cart)));
}
const editQuantityCartItem = (item: CartItemState, quantity: number) => {
editQuantityOfCartItemUseCase
.execute(item.id, quantity)
.then(cart => setState(this.mapToUpdatedState(cart)));
}
const addProductToCart = (product: Product) => {
addProductToCartUseCase
.execute(product)
.then(cart => setState(this.mapToUpdatedState(cart)));
}
const mapToUpdatedState(cart: Cart) => {
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,
};
}),
};
}
return {
state,
closeCart,
openCart,
removeCartItem,
editQuantityCartItem,
addProductToCart
};
}
A través de useState se permite suscribirse a cambios de estado.
Al usar PLoC, no permítiamos modificar el estado directamente desde los componentes, sino a través de métodos haciendo de acciones posibles.
De la misma manera si usamos custom hooks, no vamos a exponer el setState sino funciones en forma de acciones posibles a realizar también.
Ventajas de usar custom hooks
Vamos a repasar las ventajas de utilizar custom hooks en lugar PloCs como adaptadores de presentación usando Clean Architecture.
Separación de responsabilidades
Usando custom hooks como adaptadores de presentación en clean architecture seguimos manteniendo separada la lógica de presentación y la gestión estado de los componentes.
Las única responsabilidad de los componentes debe ser como renderizar la información y de que forma permitir al usuario invocar las acciones posibles.
Estado local o global
Usando custom hooks podemos gestionar el estado local de un componente o página y también estado global de la aplicación o parte de ella combinando el uso de custom hooks y API Context.
Menor fricción con React developers
Los custom hooks están muy integrados en la comunidad React. Existen muchas librerías que los usan y es ampliamente recomendado su uso.
Esto hace que sea un concepto más familiar para los desarrolladores React y puedan existir menos fricciones para aplicar Clean Architecture.
Inconvenientes de usar custom hooks
Vamos a ver también los inconvenientes de utilizar custom hooks en lugar PloCs como adaptadores de presentación usando Clean Architecture.
Acoplamiento
Los PLoCs si se crean de forma manual son independientes de cualquier tecnología o librearía, por lo tanto no estan acoplados a React.
Tener adaptadores exclusivamente dependientes del lenguaje es el escenario ideal.
Esto nos permitía reutilizar lógica de presentación y gestión de estado incluso entre tecnologías, algo que si usamos custom hooks no es posible. Nos estaríamos acoplando a React.
Inyección de dependencias menos limpia
Cuando teníamos un adaptador como un PloC modelado como una clase podemos inyectar en el constructor los casos de uso a ejecutar.
Si encima usamos composition root y este es el encargado de crear los PloCs o registrarlos, la inicialización de los PloCs es transparente para los componentes, simplemente se lo solicitaban al composition root.
Como los custom hooks son funciones no tenemos esta opcion.
La función es invocada desde el componente y es en ese momento cuando debemos de una u otra forma informar al custom hook de sus dependencias.
También existe la posibilidad de que el custom hooks solicite el mismo sus dependencias al composition root, DI injector o similar, pero esto puede llegar a ser problemático si tienes tests unitarios de custom hooks.
Conclusiones
Hemos repasado una alternativa al uso del patrón BLoC en React usando custom hooks como adaptadores de presentación.
Hemos visto sus ventajas y sus inconvenientes.
A mi personalmente me gusta más el patrón BloC o PLoC por su desacoplamiento de React y es la opción que uso en mis proyectos personales.
Sin embargo la opción de custom hooks es la opción que utilizo cuando colaboro con equipos que están empezando con clean Architecture o tienen poca experiencia en temas de arquitectura porque es más accesible para ellos y genera menos fricciones.
Remomiendo tomar las diferentes opciones posibles como herramientas y escoger la que mejor se adapta al equipo y contexto.