Este principio se corresponde con la S de SOLID.
En este artículo vamos a centrarnos en ver cómo lo podemos aplicar a la hora de construir nuestros componentes de React permitiéndonos pasar los denominados "supercomponentes" (componentes que hacen muchas cosas) a una serie de componentes mucho más pequeños que serán mucho más mantenibles y reutilizables.
¿Qué es lo que dice el SPR?
Pues que un componente de software ha de tener una y solamente una responsabilidad o, dicho de otra manera, un componente solamente deberá hacer una cosa y solamente una.
¿Qué beneficios nos va a reportar?
Reducir la complejidad de nuestras aplicaciones puesto que estas están formadas por componentes pequeños esto se traducirá en que serán mucho más sencillos de entender y facilitará nuestro trabajo con ellos.
Estaremos apostando por la reutilización de los componentes puesto que al estar todos ellos enfocados a la realización de una tarea concreta será mucho más sencillo que los volvamos a elegir para realizar la misma tarea en diferentes partes de la aplicación e incluso en aplicaciones diferentes.
Facilitaremos el testing ya que al tratarse de componentes pequeños con funcionalidades bien definidas se traducirá en que el número de casos a testear será menor.
Mejorará el mantenimiento puesto que en el momento en el que aparezca un bug será mucho más fácil aislarlo en el componente en el que se está produciendo o al menos será mucho más sencillo acotar el conjunto de componentes donde se produce.
Ejemplo
Vamos a mostrar un componente que viola el SRP con el fin de irlo refactorizando hasta lograr que sí que lo cumpla. El componente en cuestión es ProductsPage
que se encarga de renderiza la página en la que se listan todos los productos de una aplicación.
import { useQuery } from '@tanstack/react-query'
import { Product } from './types/product'
export default function ProductsPage() {
const {
data: products,
isFetching,
error
} = useQuery({
queryKey: ['products'],
queryFn: async () => {
const responde = await fetch('https://fakestoreapi.com/products')
const data = await response.json()
returns data as Product[]
}
})
return (
<div>
<h1>Products Page</h1>
{ isFetching && <p>Loading...</p> }
{ error && <p>Something went wrong</p> }
{ products && && (
<div>
{ products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.price}</p>
<div>
<h3>Seller</h3>
<p>{product.seller.name}</p>
</div>
</div>
))}
</div>
)}
</div>
)
}
Este componente está violando el SRP puesto que está haciendo muchas cosas:
hace una llamada a una API para obtener la lista de todos los productos que se han de renderizar en la página haciendo uso del hook
useQuery
.en el código JSX el componente decide qué es lo que se ha de renderizar en función del estado de la query anterior definiendo el código JSX que se encargará de renderizar en cada uno de los casos.
Obtención de la información de la API
Centrándonos en el código que va a permitir obtener la información de la API aquí tenemos que entender que no es responsabilidad de ProductsPage
saber cómo obtener todos los productos sino que simplemente querrá consumir esta información sin que le importe si esta información se obtiene con react-query o utilizando cualquier otra librería.
Así para lograr que ProductsPage
siga el SRP lo que vamos a hacer es definir un custom hook que se encargue de realizar el proceso de obtención de la información de los productos y luego pasar a utilizarlo en el componente.
import { useQuery } from '@tanstack/react-query'
import { Product } from '../types/product'
export const useFetchProducts = () => {
return useQuery({
queryKey: ['products'],
queryFn: async () => {
const responde = await fetch('https://fakestoreapi.com/products')
const data = await response.json()
returns data as Product[]
}
})
}
Este hook retorna el resultado la invocación de useQuery
de tal manera que pueda ser consumido donde se necesite dentro de la aplicación.
Pero... un momento ¿este custom hook no está haciendo a su vez dos cosas? Si es así no estará siguiendo el SRP... y así es puesto que por una parte está definiendo la clave con la que se almacenará la query gracias a react-query pero es que además se define la función gracias a la cual se obtendrán los datos. Esto quiere decir que tendremos que refactorizarlo:
import { useQuery } from '@tanstack/react-query'
import { Product } from '../types/product'
const fetchProducts = async (): Promise<Product[]> => {
const responde = await fetch('https://fakestoreapi.com/products')
const data = await response.json()
returns data as Product[]
}
export const useFetchProducts = () => {
return useQuery({
queryKey: ['products'],
queryFn: fetchProducts
})
}
Nota: se escribe la función
fetchProducts
dentro del mismo archivo que tiene el custom hook pero en un aplicación real no debería ser así ya que deberíamos situarlo dentro de una carpetaapi
o similar.
Ahora ya podemos pasar a utilizar este custom hook en el código de ProductsPage
garantizando que el proceso de obtención de la información sigue el SRP.
import { useFetchProducts } from './hooks/useFetchProducts'
export default function ProductPage() {
const { data: products, isFetching, error } = useFetchProducts()
return (
<div>
<h1>Products Page</h1>
{ isFetching && <p>Loading...</p> }
{ error && <p>Something went wrong</p> }
{ products && && (
<div>
{ products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.price}</p>
<div>
<h3>Seller</h3>
<p>{product.seller.name}</p>
</div>
</div>
))}
</div>
)}
</div>
)
}
Refactorizar el JSX
En lo primero que tenemos que pensar cuando estamos mirando el código JSX es que el componente ProductPage
está renderizando el mensaje mientras se están obteniendo los datos de la API (cuando isLoading
es true
), cuando se produce un error o bien cuando se tienen los datos de los productos. Esto en principio es así puesto que es el componente quien tiene que decidir qué se ha de renderizar en cada caso pero no es su responsabilidad determinar qué se ha de renderizar en cada caso.
Así en caso del estado isLoading
vamos a tener que crear un nuevo componente para mostrar el mensaje cuando se está obteniendo la información de la API.
export default function LoadingDisplay() {
return <p>Loading...</p>
}
Y siguiendo esta misma filosofía vamos a crear el componente que se encargará de mostrar el mensaje de error cuando sea necesario:
export default function ErrorDisplay() {
return <p>Something went wrong...</p>
}
Ahora ya podemos utilizar estos dos nuevos componentes dentro de ProductsPage
delegando la responsabilidad de mostrar esta información a los nuevos componentes:
import ErrorDisplay from './components/ErrorDisplay'
import LoadingDisplay from './components/LoadingDisplay'
import { useFetchProducts } from './hooks/useFetchProducts'
export default function ProductPage() {
const { data: products, isFetching, error } = useFetchProducts()
return (
<div>
<h1>Products Page</h1>
{ isFetching && <LoadingDisplay /> }
{ error && <ErrorDisplay /> }
{ products && && (
<div>
{ products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.price}</p>
<div>
<h3>Seller</h3>
<p>{product.seller.name}</p>
</div>
</div>
))}
</div>
)}
</div>
)
}
Con esta aproximación la ventaja que tenemos es que ahora tenemos dos nuevos componentes que se encargarán de los mensajes de carga de la información y de mostrar los mensajes de error que seguramente vayamos a utilizar más de una vez en toda nuestra aplicación. En este ejemplo estamos tratando de mantener las cosas lo más simples posibles pero en una aplicación real lo normal es que a tranto los mensajes de carga como de error estén mucho más elaborados que lo que acabamos de describir.
Vamos a centrarnos ahora con la parte de los productos donde tenemos que pensar en que el componente ProductsPage
no tiene la responsabilidad de saber cómo se han de renderizar múltiples productos en la UI. El componente sí que tiene que saber que se han de renderizar los productos pero se ha de delegar esta funcionalidad a otro componente que se encargue del renderizado.
Por lo tanto vamos a crear el componente ProductList
cuyo código sería algo como lo siguiente:
import { Product } from '../types/product'
type ProductListProps = {
products: Product[]
}
export default function ProductList({ products }: ProductListProps) {
return (
<div>
{ products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.price}</p>
<div>
<h3>Seller</h3>
<p>{product.seller.name}</p>
</div>
</div>
))}
</div>
)
}
y ya podemos pasar a utilizarlo en el componente ProductList
denjándonos el código como sigue:
import ProductList from './components/ProductList'
import ErrorDisplay from './components/ErrorDisplay'
import LoadingDisplay from './components/LoadingDisplay'
import { useFetchProducts } from './hooks/useFetchProducts'
export default function ProductPage() {
const { data: products, isFetching, error } = useFetchProducts()
return (
<div>
<h1>Products Page</h1>
{ isFetching && <LoadingDisplay /> }
{ error && <ErrorDisplay /> }
{ products && && <ProductList products={products} /> }
</div>
)
}
Como podemos ver ahora mismo el componente ProductPage
nos ha quedado bastante pequeño y con un código que es simple de mantener y que se entiende muy fácilmente.
Pero no podemos quedarnos aquí puesto que el componente ProductList
que hemos definido anteriormente no está siguiendo el SRP puesto que sí que está renderizando la lista de todos los componentes pero además está asumiendo la responsabilidad de renderizar la UI para cada uno de los productos individuales por lo que deberemos crear un neuvo componente que sea el encargado de renderizar la información que se corresponde a cada uno de los elementos individuales.
El aspecto de este componente, al que llamaremos ProductCard
, será el que se puede ver en el siguiente código, componente que sí que sigue el SRP puesto que únicamente hace una cosa (una única responsabilidad) ya que toma como prop un producto y renderiza la UI para dicho producto:
type ProductCardProps = {
product: Product
}
export default function ProductCard({ product }: ProductCardProps ) {
return (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.price}</p>
<div>
<h3>Seller</h3>
<p>{product.seller.name}</p>
</div>
</div>
)
}
Podríamos seguir aplicando el SRP dentro del componente
ProductCard
puesto que el vendedor es a su vez una persona que no tiene que ver son el producto en sí mismo y así deberíamos hacerlo (por ejemplo, definiendo el componenteUserCard
donde se renderizaría el avatar del usuario, nombre, etc.). Pero con el fin de mantener las cosas simples de cara a la explicación vamos a suponer que el vendedor forma parte del producto.
Ahora tendremos que ir al componente ProductList
y simplemente pasaremos a utilizar ProductCard
como sigue logrando también que siga el SRP puesto que su responsabilidad es renderizar una lista de productos delegando la responsabilidad de renderizar la información de cada uno de los productos a otro componente.
import { Product } from '../types/product'
type ProductListProps = {
products: Product[]
}
export default function ProductList({ products }: ProductListProps) {
return (
<div className='flex gap-4'>
{ products.map(product => <ProductCard key={product.id} product={product} /> }
</div>
)
}
Nota: hemos añadido los estilos de Tailwind necesarios para renderizar la lista de los productos puesto que es responsabilida del componente
ProductList
definir cómo se renderiza esa lista de los componentes.
Resúmen
Para saber si un componente está violando o no el SRP deberemos hacernos la siguiente pregunta ¿cuál es la responsabilidad única de este componente? El problema aquí es que la respuesta no tiene por qué ser obvia y muchas veces queda a criterio del desarrollador.
En el ejemplo de ProductsPage
podemos ver de forma intuitiva que se está violando el SPR puesto que no solamente estaba obteniendo la información de los productos sino que además pintaba la lista de todos ellos además de controlar lo que se renderizaría en los estados para obtener la información.
El SRP nos viene a decir que deberemos crear nuestros componente pensando en que únicamente han de hacer una cosa y cuando necesitemos añadir nuevas funcionalidades a los mismos la estrategia consistirá en crear un nuevo componente que la ofrezca para posteriormente vincularlos logrando el objetivo que se está persiguiendo.
Top comments (0)