Saltar al contenido

Lección 8 de 15 · Lectura y práctica

Separación de concerns: UI vs lógica vs datos

Concepto claveLa 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.

Del concepto a la práctica

Taller de esta lección

Explicación

Separar concerns significa que una regla de negocio puede probarse sin renderizar y que la UI decide presentación sin conocer el origen de los datos. La frontera debe ser observable en las funciones y sus entradas.

Ejemplo resuelto

js
const equipos = [{ id: 1, nombre: "Cámara", activo: true }, { id: 2, nombre: "Trípode", activo: false }];
const disponibles = equipos.filter(e => e.activo);
console.log(disponibles.map(e => e.nombre)); // ["Cámara"]

Los datos son registros; la lógica filtra; la UI solo recibe disponibles.

Pruébalo

  1. Coloca los registros en una fuente separada.

  2. Extrae el filtro a una función pura.

  3. Haz que el componente renderice el resultado.

Error frecuente: mezclar una regla reutilizable con detalles de presentación que impiden probarla por separado; corrígelo nombrando una función comprobable antes del retorno.

Comprueba tu resultado

Ejercicio: añade un tercer equipo activo y verifica que la función devuelve exactamente Cámara y el nuevo equipo, mientras la vista no contiene la regla activo.

Una expresión simple y pura dentro del JSX no es incorrecta por sí misma; extraerla tiene sentido cuando mejora claridad, reutilización o comprobación.

Diagrama de decisión con ramas Sí y No y un bucle que procesa cada elemento de una lista.
CursaloMapa de práctica
Cómo avanza un programa

Una condición decide qué camino ejecuta el programa. Un bucle repite una operación mientras queden elementos, y termina cuando la lista se agota. Separar ambas ideas ayuda a seguir el flujo y comprobar casos límite.

Ver la infografía en tamaño completo ↗

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}

); }

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

  1. Mezclar lógica de negocio en el renderizado: Por ejemplo, hacer cálculos complejos dentro del JSX. Solución: extraer a funciones o hooks.
  2. Poner todo en un solo contexto global: Un contexto enorme que maneja usuarios, productos, etc. Solución: crear contextos pequeños y específicos.
  3. 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.
  4. 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.
  5. 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}
))}

Carrito: {cart.length} items

); }

Pasos:

  1. Crea el hook useProducts que retorna los productos y estado de carga.
  2. Crea el CartContext con funciones addToCart, removeFromCart y el array cart.
  3. Refactoriza ProductList para que solo renderice productos y use el hook y el contexto.
  4. Crea Cart que muestre los items del carrito usando el contexto.
  5. 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).
Pistas
  • 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.

Sigue con React Intermedio: Dominando Componentes y Patrones Avanzados

Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.

Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.