Patrones de diseño en React: desde el POO clásico hasta los hooks modernos
Explica cómo los patrones de software clásicos como Singleton, Factory y Observer se aplican en React, junto con patrones específicos de React como HOCs, Hooks y Componentes compuestos.
Muchos desarrolladores que son nuevos en React no se dan cuenta de inmediato de que los patrones de diseño también se aplican fuera de los sistemas backend. Es común asumir que estos conceptos solo son relevantes para la arquitectura del lado servidor, para luego descubrir, al desarrollar aplicaciones frontend de forma profesional, que muchos de estos patrones ya se están aplicando instintivamente, sin que se les asigne un nombre consciente.
Los patrones de diseño son, en esencia, plantillas probadas y reutilizables para abordar problemas recurrentes que aparecen una y otra vez en los proyectos de software. Cuando necesitas que tu base de código permanezca organizada, bien estructurada y conectada lógicamente, estos patrones te proporcionan un plano a seguir. Funcionan como mejores prácticas codificadas que mejoran la calidad de tu código y prolongan su capacidad de mantenimiento.
Entre las mayores ventajas que aportan los patrones de diseño se encuentran la reutilización, la mantenibilidad, la escalabilidad, así como mejoras en velocidad y eficiencia. Antes de adentrarnos en los patrones específicos para React, vale la pena revisar los patrones fundamentales de ingeniería de software que existían mucho antes de los frameworks front-end.
Patrones Clásicos de Ingeniería de Software
Se trata de patrones que existen independientemente de cualquier lenguaje en particular, aplicándose ampliamente tanto en paradigmas orientados a objetos como funcionales. React mismo depende internamente de varios de ellos, y los ingenieros que trabajan en el códigobase de React los utilizan para organizar el estado, coordinar las dependencias del ciclo de vida entre componentes, reducir el tamaño del paquete y mantener la lógica de interfaz compleja comprensible.
Patrón Singleton
Este patrón garantiza que una clase u objeto tenga exactamente una instancia durante todo el tiempo de ejecución de una aplicación, al mismo tiempo que expone un único punto de acceso global a dicha instancia.
En el frontend, el patrón Singleton es útil para gestionar recursos compartidos: cosas como almacenes de estado centralizados, objetos de configuración para toda la aplicación, instancias de seguimiento analítico o un único cliente API compartido.
// Singleton API Service
class APIClient {
constructor() {
if (APIClient.instance) {
return APIClient.instance;
}
this.baseURL = "https://api.example.com";
APIClient.instance = this;
}
fetchData(endpoint) {
return fetch(`${this.baseURL}${endpoint}`).then(res => res.json());
}
}
// Any module importing/instantiating this gets the exact same instance
const client1 = new APIClient();
const client2 = new APIClient();
console.log(client1 === client2); // true
Con los módulos ES modernos, ya no se necesita lógica manual de verificación de instancias; basta con exportar un único objeto o instancia compartido para obtener automáticamente el comportamiento de singleton.
// apiClient.js
export const apiClient = new APIClient(); // ES modules cache exports automatically
Patrón Factory
El patrón Factory define una interfaz para crear objetos, sin que el código que los llama necesite conocer la clase específica o la función constructora responsable de generar ese objeto.
Este patrón resulta útil cuando es necesario generar elementos de interfaz de usuario de forma dinámica, manejar respuestas de API que llegan en diferentes formatos, o crear abstracciones que oculten las diferencias entre plataformas, como unificar la forma en que la web y los dispositivos móviles manejan los eventos de entrada.
// Button Factory for dynamically rendering UI elements
function createButton(type, config) {
switch (type) {
case 'primary':
return { role: 'btn-primary', label: config.label, onClick: config.onClick };
case 'icon':
return { role: 'btn-icon', icon: config.iconName, onClick: config.onClick };
case 'link':
return { role: 'btn-link', href: config.url };
default:
throw new Error(`Unsupported button type: ${type}`);
}
}
const primaryBtn = createButton('primary', { label: 'Submit', onClick: () => {} });
Patrón Observer
Este es el mecanismo subyacente detrás de los listeners de eventos y de las bibliotecas de gestión de estado como Redux, Zustand y MobX. Cabe señalar que la API Context de React no se basa en este patrón.
class EventEmitter {
constructor() {
this.events = {};
}
// Subscribe
on(event, listener) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(listener);
}
// Publish
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(data));
}
}
}
// Usage
const store = new EventEmitter();
// Component A subscribes to state changes
store.on('userLoggedIn', user => console.log(`Welcome, ${user.name}!`));
// Login Service triggers event
store.emit('userLoggedIn', { name: 'Sarah' });
Patrón de Módulo
Este patrón envuelve el código en un cierre para que las variables y funciones internas permanezcan privadas, exponiendo solo una interfaz pública seleccionada intencionalmente.
Antes de que estuvieran disponibles los módulos nativos ES6, esta era la técnica estándar para mantener limpio el objeto global window y lograr una verdadera privacidad de variables en JavaScript.
// Module using IIFE (Immediately Invoked Function Expression)
const ShoppingCartModule = (function () {
// Private variable
const cart = [];
// Private function
function calculateTotal() {
return cart.reduce((sum, item) => sum + item.price, 0);
}
// Public API
return {
addItem(item) {
cart.push(item);
},
getTotal() {
return calculateTotal();
}
};
})();
ShoppingCartModule.addItem({ name: 'Keyboard', price: 100 });
console.log(ShoppingCartModule.getTotal()); // 100
console.log(ShoppingCartModule.cart); // undefined (Private!)
En la actualidad, los módulos ES nativos con import y export gestionan automáticamente el ámbito de las variables, por lo que rara vez es necesario crear closures manualmente solo para mantener las variables privadas.
// cart.js
const cart = []; // Private to cart.js file
export const addItem = (item) => cart.push(item);
export const getTotal = () => cart.reduce((sum, i) => sum + i.price, 0);
Patrones de diseño de componentes específicos para React
Los patrones a continuación abordan desafíos únicos relacionados con la renderización de interfaces de usuario, el intercambio de lógica entre componentes, la gestión del estado en toda la estructura del árbol y la evitación de un uso excesivo de propiedades en profundidad.
Esta sección explica los siguientes patrones a nivel de componente:
- el patrón HOC
- el patrón basado en hooks
- el patrón de componente compuesto
- la separación entre contenedor y componente presentacional
- el enfoque de render props
- el patrón emergente de UI con IA
Patrón HOC
El Componente de Orden Superior, o HOC, es una de las primeras técnicas que ofreció React para manejar problemas transversales. Imagina un escenario en el que, una vez que un usuario opta por el seguimiento, es necesario enviar un evento de análisis tan pronto como se carga un componente. Codificar esa lógica directamente en cada componente de página se vuelve rápidamente repetitivo, y actualizarla posteriormente —por ejemplo, cuando cambia el SDK de análisis— se convierte en un problema de mantenimiento. El patrón HOC existe precisamente para resolver este tipo de problemas.
Un HOC toma un componente existente y le agrega comportamientos adicionales, sin que el componente original necesite tener conocimiento alguno de ese comportamiento adicional. Esa separación es el objetivo principal del patrón. Por ejemplo, un componente Page se concentra únicamente en la renderización, mientras que envolverlo con withAnalytics(Page) se encarga de la lógica de seguimiento por separado.
En los repositorios de código más recientes, los hooks han asumido en gran medida esta responsabilidad, pero aún se encontrará con frecuencia el patrón HOC en proyectos React heredados.
Patrón de hooks
Pocas adiciones han cambiado a React de manera tan fundamental como los hooks. Introducidos en React 16.8 en 2019, desde entonces se han convertido en el enfoque por defecto para escribir la mayor parte del código React moderno.
El patrón de hooks permite a los desarrolladores extraer y reutilizar lógica con estado y efectos secundarios entre componentes mediante funciones comunes, reemplazando así eficazmente enfoques anteriores como los HOC y las propiedades de renderizado.
Patrón compuesto
El patrón compuesto permite que un conjunto de componentes colaboren implícitamente en estado y lógica compartidos, sin necesidad de pasar todo manualmente a través de propiedades. Aparece con mayor frecuencia en elementos interactivos complejos como menús desplegables, acordeones, pestañas y menús de navegación.
Patrón de contenedor / presentacional
Este patrón impone una separación clara de responsabilidades al dividir las tareas de un componente en dos roles distintos: uno que gestiona la lógica de la aplicación y otro que se ocupa exclusivamente de renderizar la interfaz de usuario.
El componente contenedor es responsable de decidir qué datos debe ver el usuario. Controla el estado, maneja los efectos secundarios y contiene la lógica de la aplicación.
Por el contrario, el componente presentacional se enfoca en cómo se muestran esos datos. Simplemente recibe los datos y las funciones de callback a través de propiedades, sin modificar nunca los datos subyacentes en sí.
El desarrollo moderno con React tiende en gran medida hacia los hooks personalizados en lugar de esta separación entre contenedor y componente presentacional. En vez de crear un componente contenedor dedicado solo para obtener datos, se puede extraer esa lógica de obtención en un hook personalizado y llamarlo directamente desde el componente que lo necesite. Esto mantiene la separación de responsabilidades intacta al mismo tiempo que elimina capas adicionales de anidamiento de componentes y código repetitivo.
Patrón de render props
Con este patrón, una función se pasa como prop a un componente, lo que le da a dicho componente control sobre el estado y la lógica, mientras que la decisión sobre qué renderizar queda en manos de quien lo utilice.
La idea fundamental detrás de los render props es que, en lugar de que el componente contenedor renderice él mismo una interfaz de usuario predefinida, ejecuta su lógica interna y luego invoca una función como prop para generar el JSX resultante.
En React actual, los hooks personalizados han asumido en su mayor parte el papel de las propiedades render que antes se utilizaban para compartir lógica de datos pura. No obstante, las propiedades render siguen siendo útiles cuando un componente necesita gestionar y controlar todo un subárbol, al tiempo que permite al llamante decidir sobre el marcado. Las bibliotecas de UI sin interfaz gráfica como React Aria y TanStack Table dependen de este enfoque para implementar comportamientos complejos —como el manejo de accesibilidad, la gestión del foco y cuestiones similares— sin imponer ningún estilo o estructura DOM específica.
Patrón de UI con IA
Se trata de una adición relativamente reciente a la lista. La creación de interfaces impulsadas por IA, ya sean chatbots o asistentes inteligentes más generales, exige una coordinación cuidadosa entre los servicios de IA en el backend y la capa de interfaz de usuario reactiva. El patrón UI de IA consiste esencialmente en conectar los backends de modelos de lenguaje grandes con interfaces de cliente responsivas para que puedan manejar intercambios conversacionales, respuestas en streaming y ejecución asíncrona del modelo de manera fluida.
Una de las ideas centrales aquí es mantener el backend y la capa de proxy separados del cliente. Para evitar exponer claves API y gestionar adecuadamente la carga computacional, todas las llamadas relacionadas con la IA deben pasar por una capa del lado del servidor, como los gestores de rutas de Next.js o un proxy API de Node.js situado delante de Vite. Llamar directamente a los servicios de IA desde el navegador es algo que se debe evitar por completo.
Lecturas relacionadas
- Tres patrones de TypeScript que mejoran la arquitectura de apps React — Aprenda cómo los patrones Repository, Observer y Builder utilizan el sistema de tipos de TypeScript para crear bases de código en React y Next.js más limpias y fáciles de mantener.
- Tipado de React Hooks: useState, useEffect, useReducer y gancho personalizados — Aprenda cómo tipar correctamente useState, useEffect, useReducer y los gancho personalizados en TypeScript, además de cuándo elegir TypeScript en lugar de JavaScript puro realmente trae beneficios.