Concepto clave
La separación de concerns (SoC) es un principio de diseño que consiste en dividir un sistema en partes distintas, cada una responsable de una funcionalidad específica. En React, esto se traduce en separar la UI (cómo se ve), la lógica (cómo funciona) y los datos (de dónde vienen). Una analogía del mundo real es un restaurante: el chef (lógica) prepara la comida, el mesero (UI) la sirve, y la despensa (datos) almacena los ingredientes. Si todo estuviera mezclado, sería un caos.
En aplicaciones React, cuando no separamos estos aspectos, los componentes se vuelven difíciles de mantener, probar y reutilizar. Por ejemplo, un componente que hace fetch de datos, los procesa y renderiza una tabla está haciendo demasiadas cosas. Al separar, cada parte puede evolucionar independientemente.
La separación no significa archivos separados por fuerza, sino responsabilidades claras. Un componente puede tener lógica interna si es de presentación, pero la lógica de negocio y el acceso a datos deben estar en capas distintas, típicamente usando hooks personalizados o Context API.
Cómo funciona en la práctica
Supongamos que tenemos un componente que muestra una lista de usuarios. Inicialmente, todo está mezclado:
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return Cargando...
;
return (
{users.map(user => - {user.name}
)}
);
}Este componente mezcla datos (fetch), lógica (estado de carga) y UI (renderizado). Para separar, creamos un hook personalizado useUsers que maneja los datos y la lógica:
// hooks/useUsers.js
import { useState, useEffect } from 'react';
export function useUsers() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return { users, loading };
}Luego, el componente solo se encarga de la UI:
// components/UserList.js
import { useUsers } from '../hooks/useUsers';
function UserList() {
const { users, loading } = useUsers();
if (loading) return Cargando...
;
return (
{users.map(user => - {user.name}
)}
);
}Ahora, si queremos cambiar la fuente de datos, solo modificamos el hook. Si queremos cambiar el diseño, solo tocamos el componente. Esto es la separación de concerns.
Código en acción
Veamos un ejemplo más completo usando Context API para manejar datos globales. Supongamos que tenemos un contexto de autenticación:
// context/AuthContext.js
import { createContext, useState, useContext } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (username, password) => {
// Lógica de autenticación
setUser({ name: username });
};
const logout = () => setUser(null);
return (
{children}
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth debe usarse dentro de AuthProvider');
return context;
}Aquí, el contexto maneja los datos (user) y la lógica (login/logout). Los componentes que consumen este contexto solo se preocupan por la UI:
// components/Profile.js
import { useAuth } from '../context/AuthContext';
function Profile() {
const { user, logout } = useAuth();
if (!user) return No has iniciado sesión.
;
return (
Bienvenido, {user.name}
Cerrar sesión
);
}El componente Profile no sabe cómo se autentica, solo muestra datos y dispara acciones. El contexto AuthProvider es quien orquesta la lógica y los datos.
Errores comunes
- Mezclar lógica de negocio en el renderizado: Por ejemplo, hacer cálculos complejos dentro del JSX. Solución: extraer a funciones o hooks.
- Poner todo en un solo contexto global: Un contexto enorme que maneja usuarios, productos, etc. Solución: crear contextos pequeños y específicos.
- No separar la obtención de datos de la UI: Tener fetch directamente en componentes de presentación. Solución: usar hooks personalizados o bibliotecas como React Query.
- Ignorar la separación en componentes pequeños: Incluso un botón puede tener lógica interna que debería estar en un hook. Ejemplo: un botón que llama a una API al hacer clic. Solución: mover la llamada a un hook.
- No usar Context para datos globales: Pasar props por muchos niveles (prop drilling) en lugar de usar Context. Solución: identificar datos que muchos componentes necesitan y envolverlos en un Provider.
Checklist de dominio
- Identifico al menos tres capas en mi aplicación: UI, lógica y datos.
- He extraído la lógica de negocio en hooks personalizados.
- Los componentes de presentación no contienen llamadas a APIs ni lógica compleja.
- Uso Context API para datos globales (autenticación, tema, etc.) en lugar de prop drilling.
- Puedo cambiar la fuente de datos (ej: de REST a GraphQL) sin modificar componentes de UI.
- Puedo cambiar el diseño de un componente sin afectar la lógica de negocio.
- He creado un archivo de hook por cada entidad o funcionalidad (ej: useUsers, useAuth).
Refactoriza un componente mezclado aplicando separación de concerns
Objetivo: Tomar un componente que mezcla UI, lógica y datos, y separarlo en capas usando hooks y Context API.
Entregable: Un archivo ZIP con los siguientes archivos:
components/ProductList.js(solo UI)hooks/useProducts.js(lógica y datos)context/CartContext.js(estado global del carrito)components/Cart.js(UI del carrito)
Parte del código original (mezclado):
function ProductList() {
const [products, setProducts] = useState([]);
const [cart, setCart] = useState([]);
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(setProducts);
}, []);
const addToCart = (product) => {
setCart([...cart, product]);
};
return (
Productos
{products.map(p => (
{p.name}
addToCart(p)}>Agregar
))}
Carrito: {cart.length} items
);
}Pasos:
- Crea el hook
useProductsque retorna los productos y estado de carga. - Crea el
CartContextcon funcionesaddToCart,removeFromCarty el arraycart. - Refactoriza
ProductListpara que solo renderice productos y use el hook y el contexto. - Crea
Cartque muestre los items del carrito usando el contexto. - Envuelve la aplicación con
CartProvider.
Mini-rúbrica de evaluación:
- El hook useProducts no tiene JSX (solo lógica y datos).
- CartContext no tiene JSX (solo estado y funciones).
- ProductList no tiene lógica de negocio ni llamadas a API.
- Cart solo consume datos del contexto y renderiza.
- La aplicación funciona correctamente (productos se cargan, se pueden agregar al carrito).
- El hook useProducts debe usar useState y useEffect internamente, y retornar { products, loading }.
- CartContext debe proveer cart, addToCart y removeFromCart. addToCart debe evitar duplicados o simplemente agregar al array.
- Para consumir el contexto en ProductList, usa useContext(CartContext) o crea un hook useCart.