Componentes Presentacionales
¿Qué son los Componentes Presentacionales?
Componentes Presentacionales es el patrón que usamos en Creative Minds para separar completamente la lógica de estado de la presentación visual. Dividimos cada componente complejo en dos partes:
- Custom Hook: Maneja toda la lógica (estado, efectos, validaciones, API calls)
- Componente Presentacional: Solo se encarga del JSX y la estructura visual
Regla de Oro en Creative Minds
SIEMPRE usa este patrón cuando tu Client Component tenga más de un useState y un useEffect.
Si tu componente empieza a verse así, es momento de separarlo:
// 🚨 Señal de que necesitas separar lógica de presentación
function MyComponent() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [filters, setFilters] = useState({});
useEffect(() => { /* efecto 1 */ }, []);
useEffect(() => { /* efecto 2 */ }, [data]);
useEffect(() => { /* efecto 3 */ }, [filters]);
// Más lógica...
return <div>{/* JSX complejo */}</div>;
}Por Qué Separamos Lógica de Presentación
El problema: Cuando mezclas lógica y JSX en el mismo componente, el código se vuelve difícil de leer, testear y mantener. Es como tener la cocina y el comedor en el mismo espacio - funciona, pero es caótico.
La solución: Separas la “cocina” (lógica) del “comedor” (presentación). Cada uno tiene su propósito específico y es más fácil de manejar.
Ejemplo Básico: Modal
Antes - Todo mezclado (MAL):
// 🚨 Lógica y presentación mezcladas - difícil de leer y testear
function UserModal() {
const [isOpen, setIsOpen] = useState(false);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (isOpen) {
setLoading(true);
fetch('/api/user')
.then(res => res.json())
.then(setUser)
.finally(() => setLoading(false));
}
}, [isOpen]);
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') setIsOpen(false);
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, []);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<div className="modal">
{loading ? <div>Loading...</div> : <div>{user?.name}</div>}
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
)}
</div>
);
}Después - Separado (BIEN):
// ✅ Hook con toda la lógica
function useUserModal() {
const [isOpen, setIsOpen] = useState(false);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
// Cargar usuario cuando se abre
useEffect(() => {
if (isOpen) {
setLoading(true);
fetch('/api/user')
.then(res => res.json())
.then(setUser)
.finally(() => setLoading(false));
}
}, [isOpen]);
// Cerrar con Escape
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') setIsOpen(false);
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, []);
return {
isOpen,
user,
loading,
open: () => setIsOpen(true),
close: () => setIsOpen(false),
};
}
// ✅ Componente presentacional - solo JSX limpio
function UserModal() {
const { isOpen, user, loading, open, close } = useUserModal();
return (
<div>
<button onClick={open}>Open Modal</button>
{isOpen && (
<div className="modal">
{loading ? <div>Loading...</div> : <div>{user?.name}</div>}
<button onClick={close}>Close</button>
</div>
)}
</div>
);
}Ventajas inmediatas:
- Más fácil de leer: El componente solo tiene JSX
- Más fácil de testear: Puedes testear la lógica del hook por separado
- Reutilizable: Puedes usar
useUserModalen otros componentes
Ejemplo Práctico: Search
El problema: Necesitas un buscador con filtros, paginación y debounce.
Paso 1: Hook con toda la lógica
// src/features/search/hooks/useSearch.ts
function useSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [filters, setFilters] = useState({});
// Debounce para no hacer búsquedas en cada tecla
useEffect(() => {
const timeoutId = setTimeout(() => {
if (query.length > 2) {
performSearch();
}
}, 300);
return () => clearTimeout(timeoutId);
}, [query, filters]);
// Función de búsqueda
const performSearch = async () => {
setLoading(true);
try {
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
setResults(data.results);
} finally {
setLoading(false);
}
};
return {
query,
results,
loading,
filters,
setQuery,
setFilters,
clear: () => {
setQuery('');
setResults([]);
}
};
}Paso 2: Componente presentacional limpio
// src/features/search/components/search.tsx
function Search() {
const {
query,
results,
loading,
setQuery,
clear
} = useSearch();
return (
<div className="search">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{query && <button onClick={clear}>Clear</button>}
{loading && <div>Searching...</div>}
<div className="results">
{results.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
</div>
);
}Paso 3: Reutilizar la lógica en otro componente
// src/features/search/components/compact-search.tsx
function CompactSearch() {
const { query, results, setQuery } = useSearch();
return (
<div className="compact-search">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Quick search..."
/>
{results.slice(0, 3).map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
}Beneficio: La misma lógica de búsqueda funciona para el componente completo y el compacto.
Cuándo Aplicar Este Patrón
SIEMPRE úsalo cuando:
Tu Client Component tiene más de un useState y un useEffect
// 🚨 Si tu componente se ve así, sepáralo YA
function MyComponent() {
const [loading, setLoading] = useState(false); // useState #1
const [data, setData] = useState(null); // useState #2
const [error, setError] = useState(null); // useState #3
const [filters, setFilters] = useState({}); // useState #4
useEffect(() => { /* fetch data */ }, []); // useEffect #1
useEffect(() => { /* handle filters */ }, [filters]); // useEffect #2
useEffect(() => { /* cleanup */ }, []); // useEffect #3
// Si tienes esto, necesitas separar lógica de presentación
}Ejemplos específicos donde SIEMPRE lo usas:
- Formularios con validación y manejo de errores
- Búsquedas con debounce, filtros y paginación
- Modales con estado de apertura, datos y efectos
- Listas con filtrado, ordenamiento y carga
- Dashboards con múltiples fuentes de datos
- Navegación con estado responsive y scroll
NO lo uses cuando:
- Componente simple con solo un
useStatebásico - Componente estático que no maneja estado
- Server Components (no tienen estado del cliente)
// ✅ Esto NO necesita separación - es simple
function SimpleCounter() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}Mejores Prácticas
1. Un Hook, Una Responsabilidad
Cada hook debe manejar una sola cosa:
// ✅ BIEN - Hook enfocado
function useSearch() {
// Solo lógica de búsqueda
}
function useFilters() {
// Solo lógica de filtros
}
// ❌ MAL - Hook que hace demasiado
function useEverything() {
// búsqueda, filtros, usuario, tema, notificaciones...
}2. Componente = Solo JSX
El componente presentacional no debe tener lógica:
// ✅ BIEN - Solo estructura visual
function UserProfile() {
const { user, loading, updateUser } = useUserProfile();
return (
<div className="user-profile">
{loading ? <Spinner /> : <UserInfo user={user} />}
<button onClick={updateUser}>Update</button>
</div>
);
}
// ❌ MAL - Lógica mezclada
function UserProfile() {
const [user, setUser] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
// Lógica compleja aquí - NO!
}, []);
return <div>{/* JSX */}</div>;
}3. Nombres Claros
Usa nombres que indiquen qué hace cada parte:
// ✅ BIEN - Nombres descriptivos
function useProductSearch() { /* lógica */ }
function ProductSearchForm() { /* presentación */ }
function useShoppingCart() { /* lógica */ }
function CartDrawer() { /* presentación */ }
// ❌ MAL - Nombres genéricos
function useData() { /* ¿qué datos? */ }
function Component() { /* ¿qué componente? */ }Resumen
Componentes Presentacionales es el patrón estándar en Creative Minds para cualquier Client Component con lógica compleja.
Regla simple: Más de un useState + un useEffect = separar en hook + componente presentacional.
Beneficios:
- Código más limpio y fácil de leer
- Lógica reutilizable entre componentes
- Más fácil de testear y mantener
- Mejor organización del código