Skip to Content
DocumentaciónPatronesComponentes Presentacionales

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:

  1. Custom Hook: Maneja toda la lógica (estado, efectos, validaciones, API calls)
  2. 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 useUserModal en otros componentes

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 useState bá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
Last updated on