Frameworks & Tecnologías
25 tecnologías · 5 categorías · Guía de cuándo usar cada una
Frontend UI
Meta-fw
Backend
CMS
No-Code
★Gana
◆Válido
·No ideal
Diseño
Design Systems · Herramientas de UI · Gestión de color · Accesibilidad
Métricas de Design Systems
Lista exhaustiva · fuente: design_system_metrics_exhaustive_v2
Adopción no es solo "cuánto se usa." Lo más importante es si se usa correctamente y por qué razón cuando no se usa.
Adopción en diseño (Figma)
- % de componentes usados del total publicado en la librería
- Tasa de detach — cuántos componentes se desvinculan. Señala gaps del sistema
- Frecuencia de uso por componente — cuáles se usan casi nunca
- Equipos activos que han insertado componentes en el último mes
- % de archivos de producto que usan la librería conectada
- Adopción de tokens (colores, tipografía, espaciado) además de componentes
- Adopción de variables y estilos de la librería
Adopción en código
- Cobertura — % de UI construida con componentes del DS vs custom
- Instancias en producción de cada componente (no solo en diseño)
- Desviaciones activas — componentes custom que no son del DS
- Versión adoptada — cuántos proyectos siguen en versiones antiguas
- Descargas/dependencias npm del paquete del DS
- Cobertura de tokens en CSS/variables del código base
- Adopción por equipo o repositorio — para identificar quién no usa
Adopción de documentación
- Visitas a la doc (páginas vistas, usuarios únicos, tiempo de sesión)
- Páginas más visitadas vs páginas que nadie visita
- Tasa de rebote — si entran y salen sin navegar
- Búsquedas sin resultado — qué busca la gente y no encuentra
- Tickets de soporte por temas que deberían estar en la doc
- Tiempo hasta encontrar lo que necesitan (en tests de usabilidad)
Adopción cualitativa
- Awareness — ¿los equipos saben que el DS existe?
- Percepción — ¿lo ven como una herramienta o como una restricción?
- Participación en office hours, reuniones del DS
- NPS interno — ¿lo recomendarían a otro equipo?
- Razones de no uso — por qué no lo adoptan (encuestas)
- Scorecard por equipo — autoevaluación del nivel de adopción
La eficiencia tiene dos caras: la del equipo que hace el DS, y la de los equipos que lo consumen.
Velocidad de entrega
- Time to market — antes vs después del DS
- Tiempo de diseño de pantalla estándar (benchmarking con/sin DS)
- Tiempo de desarrollo de un componente nuevo vs reusar uno
- Tiempo de handoff diseño → desarrollo (¿mejora con la doc del DS?)
- Ciclos de revisión — cuántas iteraciones necesita un diseño hasta aprobarse
- Lead time de un componente nuevo desde petición hasta publicación
Ahorro de tiempo y coste
- Horas ahorradas por reutilización de componentes (tiempo estimado vs construir desde cero)
- Coste de diseño duplicado — cuántos equipos hacen lo mismo por separado
- Reducción de deuda técnica en CSS o estilos redundantes
- Coste de refactoring evitado gracias a componentes estandarizados
- Ratio de reutilización — cuántos componentes reutilizados vs creados desde cero por sprint
Calidad del proceso
- Bugs visuales en producción relacionados con inconsistencias de UI
- Bugs evitados por tener componentes testeados de base
- Tasa de revisiones de QA antes vs después de adoptar el DS
- Errores de handoff — discrepancias entre diseño y código final
- Tiempo de onboarding de nuevos diseñadores/devs hasta ser autosuficientes
Salud del propio DS
- Cadencia de publicación — cada cuánto se actualiza el DS
- Backlog pendiente — peticiones acumuladas sin resolver
- Tiempo de respuesta a issues o peticiones de equipos
- % de componentes deprecated que aún se usan
- Cobertura Figma-código — cuántos componentes existen en ambos lados
- Número de librerías activas — síntoma de fragmentación si hay muchas
La consistencia es medible. No hay que quedarse en la percepción subjetiva.
Consistencia visual
- Redundancia CSS — duplicados de colores, tipografías, espaciados
- Estilos custom fuera del DS en archivos de diseño
- Variaciones no documentadas de componentes en producción
- Divergencia visual entre productos del mismo ecosistema
- Parity diseño-código — % de componentes de Figma con equivalente exacto en código
Accesibilidad
- % de componentes que pasan WCAG AA o AAA
- Ratio de contraste en tokens de color
- Cobertura de ARIA en componentes interactivos
- Regresiones de accesibilidad introducidas en updates
- Tests automatizados de a11y que pasan en CI/CD
- Issues de accesibilidad reportados en producción
Usabilidad del propio DS
- SUS score (System Usability Scale) aplicado al DS como producto interno
- Tasa de éxito en tareas clave de la doc (¿pueden encontrar lo que buscan?)
- Tasa de error al implementar un componente por primera vez
- Tiempo en completar tareas con el DS vs sin él
- Claridad percibida de la documentación (escala 1–5 en encuestas)
Impacto en el usuario final
- Quejas de usuarios sobre inconsistencias visuales o de interacción
- Tasa de error en tareas clave de los productos (refleja calidad de UI)
- NPS o CSAT de los productos como proxy de coherencia de experiencia
- Satisfacción con la UI en investigación de usuario periódica
- Flujos coherentes — ¿los usuarios reconocen patrones entre productos?
Las métricas de equipo son las más ignoradas y las más importantes. Un DS que nadie quiere usar no vale nada.
Satisfacción y motivación
- Satisfacción laboral — encuestas periódicas sobre cómo se siente el equipo
- Aumento de creatividad percibida — ¿el DS libera tiempo para innovar?
- Rotación del equipo de diseño como señal de motivación
- Compromiso — participación voluntaria en mejoras del DS
- Autonomía percibida — ¿sienten que el DS los limita o los empodera?
- Framework AMP — autonomía, maestría y propósito (Daniel Pink)
Contribución y gobernanza
- Contribuciones externas — propuestas de componentes de equipos no-core
- Pull requests o issues creados por equipos consumidores
- Participación en reuniones de DS (office hours, sincs)
- Feedback recibido sobre el DS — volumen y calidad
- Peticiones de nuevos componentes vs peticiones resueltas
- Embajadores del DS — personas en otros equipos que lo promueven
Colaboración entre disciplinas
- Fricción diseño-dev — quejas cruzadas, malentendidos en handoff
- Alineación percibida con producto, marketing, ingeniería
- Ritmo de sincronización — ¿se mantienen reuniones regulares de DS?
- Uso compartido — diseñadores y devs consultan la misma fuente de verdad
- Decisiones bloqueadas por falta de un componente o patrón definido
Cultura de diseño
- Visibilidad del DS en la organización — ¿los stakeholders lo conocen?
- Apoyo ejecutivo — inversión y reconocimiento desde arriba
- Evangelización — cuánta gente habla del DS proactivamente
- Percepción de valor por parte de equipos no-diseño
- Sentimiento de "alivio" (Dann Mall) — el DS reduce la carga mental
Conectar el DS con métricas de negocio es lo que le da peso frente a stakeholders.
ROI y valor económico
- Coste de mantener el DS vs coste de no tenerlo (inconsistencias, bugs, duplicación)
- Horas de diseño/dev ahorradas × coste hora → valor en €
- Reducción de bugs en prod y su coste de resolución
- Velocidad de lanzamiento de nuevas features — ¿el DS acelera?
- Coste de onboarding antes vs después del DS
Impacto en producto
- Tasa de conversión en flujos rediseñados con el DS
- Retención de usuarios como proxy de calidad de experiencia
- Reducción de soporte por confusión de UI
- Velocidad de escala — cuántos productos nuevos se lanzan usando el DS
- Consistencia de marca — percepción externa de coherencia
OKRs y frameworks
- OKRs del DS — objetivos trimestrales con resultados clave medibles
- Alineación con OKRs de producto — ¿el DS contribuye a métricas del negocio?
- Progreso de madurez — modelo de madurez del DS (inicial → gestionado → optimizado)
- Escalabilidad — cuántos productos puede soportar el DS actual
- Cobertura de superficie — % del portfolio que usa el DS
Métricas de DesignOps
- Eficacia del equipo de diseño — % de tareas completadas vs planificadas
- Iteraciones necesarias para refinar un diseño hasta aprobarse
- Frecuencia de errores de usuario en diseños producidos con el DS
- Tasa de éxito de tarea — usuarios que completan una tarea en los productos
- Tiempo en completar tareas por usuarios en los productos
El tamaño del equipo cambia qué medir, cómo medirlo y qué hacer con los datos.
Equipos grandes (10+ diseñadores)
- Adopción por equipo/squad — identifica quién no usa y por qué
- Gobernanza formal — métricas de contribuciones, PRs, aprobaciones
- Cobertura en código con herramientas automáticas (react-scanner, Omlet)
- Versión adoptada por repositorio — evitar fragmentación de versiones
- Dashboards en tiempo real de uso de componentes
- SUS score del DS como producto interno formal
- NPS interno con encuestas periódicas estructuradas
- Embajadores por equipo como multiplicadores de adopción
- ROI documentado para justificar inversión ejecutiva
- OKRs formales del equipo del DS alineados con la empresa
- Tiempo de respuesta a peticiones de equipos consumidores
Equipos pequeños (2–5 diseñadores)
- Adopción binaria primero — ¿se usa? ¿lo conocen todos?
- Conversaciones directas en vez de encuestas — más fácil y más rico
- Tiempo ahorrado percibido por los propios diseñadores
- Detach rate en Figma — señal práctica de gaps sin necesitar herramientas
- Backlog de componentes pendientes — proxy de salud del DS
- Riesgo de abandono — si no se siente útil, el DS muere solo
- Documentación accesible — ¿puedes explicarlo a alguien nuevo en 10 min?
- Coste de mantenimiento — ¿cabe en la carga del equipo?
- Flexibilidad percibida — si es muy rígido, se abandona
- Métricas cualitativas primero; automatizar solo cuando haya masa crítica
Tríada: 1 designer + 1 PM + 1–3 devs
Qué medir
- Fricción en handoff — el dev trabaja directamente con el diseñador, cualquier ambigüedad del DS se nota inmediatamente
- Tiempo de implementación por componente — el dev es quien más sufre si el DS no tiene tokens o doc clara
- Detach rate — en triadas es normal que el dev adapte porque el DS no cubre el caso de uso exacto
- Alineación PM-diseño — ¿el PM conoce qué existe en el DS antes de definir features?
- Velocidad de prototipado — si el DS acelera las demos al PM o los primeros wireframes
- Deuda acumulada — componentes custom que "ya arreglaremos" y nunca se arreglan
Particularidades clave
- El designer hace y consume el DS — es al mismo tiempo quien lo mantiene y quien lo usa, lo que crea un sesgo enorme
- El dev puede volverse el DS — si hay 2–3 devs, uno puede desarrollar un sistema propio en código que diverge del diseño
- El PM decide sin datos de DS — si el PM no tiene visibilidad de qué componentes existen, pedirá cosas que ya están resueltas
- No hay quién mida — en una tríada nadie tiene el rol de DesignOps, así que medir tiene que ser casi automático
- El DS puede ser informal — a veces es solo una librería de Figma y un Notion; lo relevante es si reduce las conversaciones repetidas
Señales prácticas (sin infraestructura)
- ¿Cuántas veces por sprint el dev pregunta al designer sobre estilos o comportamientos ya definidos?
- ¿El PM revisa el DS antes de escribir una historia de usuario?
- ¿Hay componentes en código que el designer no sabe que existen?
- ¿Cuánto tarda un nuevo dev en entender qué hay en el DS y empezar a usarlo?
- ¿Se abre Figma para reusar o se clona código de otro sitio?
- ¿El DS aparece en la retro? — si aparece como problema, está fallando
Riesgo principal de la tríada: que el DS sea invisible para el PM y el dev acabe construyendo su propio sistema de facto en código. La métrica más valiosa aquí es si los tres hablan el mismo lenguaje cuando se refieren a un componente — si no, el DS no está funcionando.
Glosario de Design Systems
Terminología esencial · busca, filtra, estudia
Herramientas de Design Systems & UI
Glosario personal · Añade nuevas herramientas con un clic
Color Tips & Gestión de Color
Base teórica y práctica para trabajar color con criterio en producto digital: modos de color, rampas, contraste y tokenización
Fundamentos aplicables a producto real
Esta sección es viva: iremos añadiendo buenas prácticas, criterios de decisión y ejemplos para diseño y desarrollo.
Accesibilidad
WCAG 2.2 · Principios POUR · Criterios · ARIA · Herramientas
Accesibilidad no es una feature opcional. Es diseñar para que cualquier persona — con o sin discapacidad visual, motora, cognitiva o auditiva — pueda usar tu producto. Las WCAG del W3C se articulan sobre 4 principios agrupados en el acrónimo POUR.
P
Perceptible
La información y los componentes de la interfaz deben ser presentados de manera que puedan percibirse visual, táctil o auditivamente.
alt text en imágenessubtítulos en vídeocontraste de colorno solo color para transmitir info
O
Operable
Los componentes y la navegación deben ser operables independientemente del dispositivo de entrada que use el usuario.
navegable con tecladosin trampas de focotiempo suficientesin contenido que cause ataques
U
Comprensible
La información y el funcionamiento deben ser comprensibles. El contenido debe ser legible y predecible, y los formularios deben ayudar a evitar y corregir errores.
idioma de la página declaradonavegación predecibleerrores identificadosetiquetas en formularios
R
Robusto
El contenido debe ser interpretado de forma fiable por una amplia gama de agentes de usuario, incluidas las tecnologías de asistencia como lectores de pantalla.
HTML semántico válidoroles ARIA correctosnombre + rol + valorcompatible con AT
A
Mínimo
Criterios básicos sin los cuales algunos usuarios no pueden acceder al contenido en absoluto.
Todo sitio público. Base legal en la mayoría de países.
AA
Estándar · El objetivo real
El nivel objetivo para la mayoría de webs y apps. Incluye contraste 4.5:1, navegación por teclado completa, texto escalable, reflow a 320px y los nuevos criterios de WCAG 2.2.
Obligatorio en sector público UE (Directiva 2016/2102). Estándar de facto de la industria.
AAA
Avanzado · Aspiracional
Criterios de máxima accesibilidad. El W3C no lo recomienda como requisito global. Se aplica a secciones específicas o audiencias especiales.
Plataformas especializadas, contenido crítico, audiencias con necesidades específicas.
Ratios de contraste · referencia rápida
Texto normal (<18pt / <14pt bold)AA 4.5 : 1AAA 7 : 1
Texto grande (≥18pt / ≥14pt bold)AA 3 : 1AAA 4.5 : 1
Componentes UI e iconos informativosAA 3 : 1— no aplica
Texto decorativo / logotipos— exento— exento
WCAG 2.2 · Publicada octubre 2023. Los criterios marcados con new 2.2 son nuevos respecto a WCAG 2.1.
PPerceptible
| Criterio | Descripción | Nivel |
|---|---|---|
| 1.1.1 Non-text Content | Todas las imágenes e iconos tienen alternativas en texto | A |
| 1.3.1 Info and Relationships | La información transmitida visualmente también está disponible programáticamente | A |
| 1.4.1 Use of Color | El color no es el único medio visual para transmitir información | A |
| 1.4.3 Contrast (Minimum) | 4.5:1 para texto normal, 3:1 para texto grande | AA |
| 1.4.4 Resize Text | El texto se puede redimensionar al 200% sin pérdida de funcionalidad | AA |
| 1.4.6 Contrast (Enhanced) | 7:1 para texto normal, 4.5:1 para texto grande | AAA |
| 1.4.10 Reflow | El contenido reflota a 320px de ancho sin scroll horizontal | AA |
| 1.4.11 Non-text Contrast | Componentes UI tienen contraste 3:1 | AA |
OOperable
| Criterio | Descripción | Nivel |
|---|---|---|
| 2.1.1 Keyboard | Toda la funcionalidad disponible vía teclado | A |
| 2.1.2 No Keyboard Trap | El foco de teclado se puede mover desde cualquier componente | A |
| 2.4.1 Bypass Blocks | Skip link o navegación por landmarks disponible | A |
| 2.4.3 Focus Order | El orden de foco preserva el significado | A |
| 2.4.4 Link Purpose | El propósito del enlace es claro por su texto o contexto | A |
| 2.4.6 Headings and Labels | Los encabezados y labels son descriptivos | AA |
| 2.4.7 Focus Visible | El indicador de foco es visible | AA |
| 2.4.11 Focus Not Obscured new 2.2 | El elemento enfocado no queda completamente oculto por contenido del autor | AA |
| 2.5.7 Dragging Movements new 2.2 | Las acciones de arrastre tienen alternativas de puntero único | AA |
| 2.5.8 Target Size (Minimum) new 2.2 | Los objetivos interactivos son al menos 24×24 CSS px | AA |
UComprensible
| Criterio | Descripción | Nivel |
|---|---|---|
| 3.1.1 Language of Page | El idioma por defecto está especificado en el HTML | A |
| 3.2.3 Consistent Navigation | La navegación es consistente entre páginas | AA |
| 3.3.1 Error Identification | Los errores de entrada están claramente descritos | A |
| 3.3.2 Labels or Instructions | Los campos de formulario tienen labels o instrucciones | A |
| 3.3.3 Error Suggestion | Se sugieren correcciones de errores cuando es posible | AA |
| 3.3.8 Accessible Authentication new 2.2 | No se requiere test de función cognitiva para login sin alternativa | AA |
RRobusto
| Criterio | Descripción | Nivel |
|---|---|---|
| 4.1.2 Name, Role, Value | Los componentes UI tienen nombres accesibles y roles correctos | A |
| 4.1.3 Status Messages | Los mensajes de estado son anunciados a los lectores de pantalla | AA |
ARIA (Accessible Rich Internet Applications) añade semántica a elementos que HTML no describe por sí solo. Regla de oro: usa siempre HTML semántico nativo primero — ARIA complementa, no reemplaza.
Botones
Usa
<button> nativo siempre que sea posible. Añade aria-label cuando el texto visible no sea suficientemente descriptivo.<button>Guardar cambios</button> <button aria-label="Cerrar diálogo">×</button> <!-- Si no puedes usar button: --> <div role="button" tabindex="0" aria-pressed="false">Toggle</div>
Formularios
Cada input necesita un
<label> asociado. Usa aria-describedby para instrucciones y aria-invalid + aria-errormessage para errores.<label for="email">Correo electrónico</label> <input type="email" id="email" aria-describedby="email-hint" aria-invalid="false"> <p id="email-hint">Nunca compartiremos tu email.</p>
Navegación
Usa
<nav> con aria-label para distinguir múltiples navs. Indica la página actual con aria-current="page".<nav aria-label="Principal"> <ul> <li><a href="/" aria-current="page">Inicio</a></li> <li><a href="/sobre">Sobre nosotros</a></li> </ul> </nav>
Modales / Diálogos
Usa
role="dialog" + aria-modal="true" + aria-labelledby. El foco debe moverse al modal al abrirse y volver al trigger al cerrarse.<div role="dialog" aria-modal="true" aria-labelledby="dialog-title"> <h2 id="dialog-title">Confirmar acción</h2> <button autofocus>Cancelar</button> </div>
Links externos
Informa siempre que un link abre en pestaña nueva. Usa texto visualmente oculto para lectores de pantalla.
<a href="https://externo.com" target="_blank" rel="noopener"> Sitio externo <span class="visually-hidden"> (abre en nueva pestaña) </span> </a>
Live Regions
Para anunciar contenido dinámico sin mover el foco.
aria-live="polite" espera; assertive interrumpe.<!-- Notificaciones no urgentes --> <div aria-live="polite" aria-atomic="true"> Guardado correctamente </div> <!-- Errores críticos --> <div role="alert"> Error: el campo es obligatorio </div>
Ninguna herramienta automatizada detecta el 100% de los problemas de accesibilidad — se estima que cubren entre el 30–40%. Son el primer filtro, no el definitivo.
El motor de testing de accesibilidad más usado. Integrable en CI/CD. Muestra problemas directamente sobre el DOM con explicaciones y enlaces a la documentación WCAG.
Visualiza los errores directamente sobre la página con iconos superpuestos. Muy visual para aprender qué está fallando y por qué.
Auditoría de accesibilidad, rendimiento, SEO y buenas prácticas integrada en Chrome DevTools. Puntúa de 0 a 100.
El lector de pantalla gratuito más usado en Windows. Prueba con NVDA + Chrome o Firefox para simular la experiencia real de un usuario con discapacidad visual.
Integrado en macOS e iOS. Actívalo con Cmd+F5. Prueba con Safari en macOS para la combinación más usada en Apple.
Visualiza cómo perciben un par de colores personas con diferentes tipos de daltonismo o baja visión. Imprescindible al definir paletas de color en un DS.
Glosario Técnico
Términos que la IA usa y que confunden · Qué significan · Cómo afectan a tu proyecto
VibeTips · Prompts Estructurales
Recetas de prompts para pedirle a la IA código de calidad con cada tecnología
Dónde Desplegar
Plataformas de hosting · Cuándo usar cada una · Compatible con qué frameworks
Regla general: Si usas Astro/Next.js → empieza en Vercel. Si usas Django/Laravel → Railway o Render. Si es solo frontend estático → Netlify o Cloudflare Pages. Si quieres BaaS → Supabase ya lo incluye.
Despliegue & Infraestructura
DNS · cPanel · Caché · Docker · Nginx · Seguridad · Integraciones CRM
El DNS es la agenda de teléfonos de internet. Traduce nombres de dominio legibles (miweb.com) a direcciones IP numéricas. Sin entender DNS, no puedes controlar dónde apunta tu web ni tu email.
Flujo: ¿Qué pasa cuando alguien escribe tu dominio?
Browserescribe miweb.com
→
Resolver DNS local¿lo tengo en caché?
→
Root DNS¿quién gestiona .com?
→
Nameserver del dominiotabla de registros DNS
→
IP del servidorregistro A
cPanel es el panel de control del servidor. Una aplicación web instalada en tu servidor (o gestionada por tu hosting como Verpex) que te permite gestionar dominios, emails, bases de datos, archivos y métricas desde una interfaz visual.
La caché es guardar una copia de algo para no tener que calcularlo o pedirlo de nuevo. Cada capa de caché tiene su lugar: no todas son iguales ni sirven para lo mismo.
Docker empaqueta tu aplicación y todo lo que necesita para correr en un contenedor portátil. Docker Compose orquesta múltiples contenedores (frontend, backend, base de datos) para que funcionen juntos con una sola configuración.
Arquitectura: Frontend + API + HubSpot (tu caso real)
Visitantebrowser HTTPS
→GET / o POST /api/*
Nginx Containerúnico punto de entrada público
→sirve estáticos directamente
Frontend estáticoHTML/JS/CSS en disco
→proxy_pass /api/*
Node.js Expressred interna · nunca expuesto
→POST + API token
HubSpot CRM APIservicio externo
La seguridad no es una capa que se añade al final. Cada capa tiene sus propias vulnerabilidades. Cloudflare protege a nivel de red, SSL cifra el canal, SSH protege el acceso al servidor, y las variables de entorno protegen las credenciales.
Integrar un CRM o servicio externo siempre sigue el mismo patrón: credenciales seguras en el backend, nunca en el frontend. El browser es público — cualquier token que pongas en el JS del cliente puede ser extraído.
Docker · Curso Completo
8 unidades · Conceptos · Comandos · Dockerfile · Compose · Redes · Volúmenes · Kubernetes · Herramientas
UD01 + UD02Introducción a contenedores y DockerConceptos base
Un contenedor no es una VM. Comparte el kernel del host pero aísla procesos, red y sistema de archivos. Más ligero, arranca en milisegundos y consume mucha menos RAM que una máquina virtual.
UD03 · UD04 · UD05 · UD06CheatSheet de comandos DockerReferencia rápida
Los comandos más usados organizados por categoría. docker para contenedores e imágenes individuales, docker compose para stacks multi-servicio.
UD04Gestión de imágenes y DockerfileBuild personalizado
Una imagen es la receta; un contenedor es el plato cocinado. El Dockerfile define paso a paso cómo construir tu imagen personalizada. Cada instrucción crea una capa — Docker reutiliza capas cacheadas para acelerar los builds.
Anatomía de un Dockerfile · Instrucciones principales
UD05Redes y volúmenes en DockerComunicación y persistencia
Sin red, los contenedores son islas. Sin volúmenes, los datos mueren con el contenedor. Redes Docker controlan qué contenedores se pueden ver entre sí. Volúmenes persisten datos más allá del ciclo de vida del contenedor.
Tipos de red
Tipos de volumen / almacenamiento
UD06Docker ComposeOrquestación multi-contenedor
Docker Compose define toda tu infraestructura en un archivo YAML. Un solo
docker compose up -d levanta todos los servicios, redes y volúmenes configurados. Es el estándar para desarrollo y despliegues pequeños.Anatomía de docker-compose.yml · Claves principales
UD07Utilidades para gestionar DockerGUIs y monitores
Más allá de la línea de comandos, hay herramientas que hacen el día a día con Docker mucho más visual y manejable. Desde extensiones de VS Code hasta paneles web completos.
UD08Introducción a KubernetesOrquestación a escala
Kubernetes (K8s) es el siguiente nivel de Docker Compose. Mientras Compose gestiona contenedores en un solo host, Kubernetes los orquesta en un clúster de máquinas: auto-scaling, self-healing, rolling updates y balanceo de carga incluidos.
Objetos principales de Kubernetes
UD03–UD08Casos prácticos del cursoLo que realmente construyes
Los casos prácticos del curso van de lo simple a lo complejo. Cada uno introduce una capa nueva: primero un contenedor solo, luego redes, luego Compose, luego stacks reales con IA y proxies.
Roadmap del Sistema
Plan de evolución de este OS · Qué viene, por qué y en qué orden
Este sistema nació como una matriz de decisión de frameworks. La conversación lo está convirtiendo en un sistema operativo personal de aprendizaje para VibeCoding: un lugar donde entender qué usas, cómo medirlo, qué significa lo que la IA te dice, y dónde vive lo que construyes. Este roadmap es el plan estructurado para llegar ahí.
Git & Documentación
Convenciones de commits · flujo add/commit/push/pull · rebase · buenas prácticas de documentación
Documentar bien y commitear bien son parte del mismo trabajo. Si el código explica el qué, el commit y la doc deben explicar el por qué.
Qué documentar siempre
- Objetivo de cada módulo o feature.
- Cómo correr el proyecto en local.
- Variables de entorno necesarias (sin secretos reales).
- Decisiones de arquitectura y trade-offs.
- Pasos de despliegue y rollback.
Dónde poner cada cosa
- README: onboarding y comandos base.
- Comentarios: lógica compleja, no obviedades.
- Mensajes de commit: propósito del cambio.
- Pull Request: contexto, impacto, riesgos, pruebas.
- CHANGELOG: cambios visibles para usuarios/equipo.
Guía completa de comandos Git: acciones clave, comando recomendado y para qué sirve cada uno en el flujo diario.
| Acción | Comando | Explicación |
|---|---|---|
| Configurar usuario | git config --global user.name "Nombre" | Define quién realiza los cambios. |
| Inicializar | git init | Crea un repositorio local nuevo. |
| Clonar | git clone [URL] | Descarga un proyecto remoto a tu PC. |
| Estado actual | git status | Muestra qué archivos has modificado. |
| Preparar cambios | git add [archivo] | Añade archivos al área de staging. |
| Guardar cambios | git commit -m "mensaje" | Crea una foto o versión del proyecto. |
| Ver cambios | git diff | Muestra las líneas exactas que modificaste. |
| Crear nueva rama | git switch -c [nombre] | Crea y te posiciona en una nueva rama. |
| Cambiar de rama | git switch [nombre] | Cambia a una rama existente. |
| Salto rápido | git switch - | Vuelve a la rama donde estabas antes. |
| Fusionar ramas | git merge [rama] | Une el trabajo de una rama a la actual. |
| Historial | git log --oneline | Muestra los commits realizados. |
| Subir cambios | git push -u origin [rama] | Sube tus commits a GitHub. |
| Descargar cambios | git pull | Trae y une el trabajo del servidor. |
| Deshacer cambios | git restore [archivo] | Descarta cambios locales no deseados. |
| Guardar temporal | git stash | Esconde cambios actuales para trabajar en otra cosa. |
| Recuperar guardado | git stash pop | Aplica los cambios que tenías escondidos. |
Consejo para tu flujo
Si alguna vez sientes que Git se ha vuelto muy complejo, vuelve a este flujo seguro:
git pull git switch -c tu-rama git add . git commit -m "..." git push
Conventional Commits ayuda a tener historial legible, changelogs automáticos y releases limpias.
| Tipo | Cuándo usarlo | Ejemplo |
|---|---|---|
| feat | Nueva funcionalidad | feat(auth): agregar login con magic link |
| fix | Corrección de bug | fix(api): manejar null en respuesta de perfil |
| docs | Cambios de documentación | docs(readme): explicar setup de Docker |
| refactor | Mejora interna sin cambiar comportamiento | refactor(ui): extraer card base reusable |
| test | Tests nuevos o ajustes de tests | test(user): cubrir flujo de registro |
| chore | Tareas de mantenimiento | chore(deps): actualizar eslint a v9 |
Formato recomendado: type(scope): mensaje corto en imperativo Ejemplos: feat(docs): crear sección de flujo git fix(glosario): mejorar contraste de etiquetas docs(commits): añadir guía de conventional commits
Flujo base seguro: trabaja en rama, haz commits pequeños, sincroniza con pull --rebase y luego push.
1
Traer cambios
git pull --rebase origin main
git pull --rebase origin main
2
Preparar cambios
git add .
git add .
3
Crear commit
git commit -m "feat(scope): mensaje"
git commit -m "feat(scope): mensaje"
4
Subir rama
git push origin mi-rama
git push origin mi-rama
Pull vs Rebase
- merge: conserva ramas tal cual, crea merge commit.
- rebase: reescribe tus commits encima de la rama base.
- ventaja rebase: historial lineal y limpio.
- regla: no rebasees commits ya compartidos por todo el equipo.
Comandos útiles
git status git log --oneline --graph --decorate git diff git rebase -i HEAD~3 git commit --amend git push --force-with-lease
Astro · Arquitectura & Islands
Cómo funciona Astro · 15 capas · React Islands · basado en el proyecto real Watan
Ejemplos basados en el proyecto real · watan-astro · los conceptos son genéricos de Astro
El flujo de datos va siempre en una dirección: los ficheros
.md son la fuente de verdad, los schemas los validan, y los templates los renderizan. Nunca al revés.Usuario visita /es/vacation-rental
↓
pages/es/vacation-rental.astro ← define la URL, pasa locale="es"
↓
templates/VacationRentalPage.astro ← maquetación completa, carga datos
↓ getEntry('vacationRentalPages', 'es/vacation-rental')
↓ Astro valida con vacationRentalSchema ← schemas/vacation-rental.schema.ts
↓ datos vienen de ← content/es/vacation-rental.md
↓
Monta HTML con components/ dentro de layouts/BaseLayout.astro
↓
Genera HTML estático (+ islands React donde hay interactividad)
Los tres pilares
Pilar 1
Contenido · los .md
src/content/es/ · en/ · ar/
La fuente de verdad del sitio. Todo texto visible —títulos, estadísticas, listas, testimonios— vive aquí en YAML estructurado, separado completamente del código.
Sin esta separación, los textos estarían dentro del HTML del template. Cambiar "75% de ocupación" requeriría abrir código y hacer un deploy. Con .md, lo edita cualquier persona que sepa escribir — sin abrir el editor, sin tocar TypeScript.
---
meta:
title: 'Gestión de Alquiler Vacacional'
hero:
heading: 'Máxima rentabilidad'
stats:
- num: '75%'
label: 'ocupación media anual'
---
Pilar 2
Validación · los schemas Zod
src/schemas/
El contrato entre el .md y el template. Define exactamente qué campos existen, de qué tipo son, y si son obligatorios u opcionales. Astro valida en build —nunca en producción.
Sin schema, escribir "ttle" en vez de "title" en el .md generaría silenciosamente una página rota que llegaría a los usuarios.
z.strictObject lo detecta en tu terminal. También evita el caso inverso: si el template espera d.hero.heading y ese campo no existe en el schema, TypeScript te avisa antes de compilar.export const vacationRentalSchema = z.strictObject({
meta: metaSchema, // obligatorio — falta → error en build
hero: heroSchema, // obligatorio
stats: z.array(
z.strictObject({
num: z.string(),
label: z.string(),
})
).optional(), // puede no existir en el .md
});
Pilar 3
Renderizado · templates + pages
src/templates/ · src/pages/
Los templates transforman datos en HTML: cargan el .md correcto según el locale, validan que exista, y componen los componentes visuales. Las pages son solo el mapeado URL → template.
Sin templates, tendrías 3 copias idénticas de toda la maquetación (una por idioma). Si cambias la sección hero, la cambias en 3 sitios a la vez, y puedes desincronizarlos. El template rompe ese patrón: hay exactamente un sitio donde vive el layout de cada página.
// pages/es/vacation-rental.astro — solo 3 líneas
import VacationRentalPage from '../../templates/...';
<VacationRentalPage locale="es" />
// template — carga el .md del idioma correcto
const page = await getEntry('vacationRentalPages', `${locale}/vacation-rental`);
const d = page.data; // TypeScript ya sabe el tipo exacto de d
Por qué esta separación
| Separación | Beneficio |
|---|---|
| Contenido en .md | No-desarrolladores pueden editar textos sin tocar código |
| Schemas en .ts | Errores detectados antes de publicar, no en producción |
| Templates separados de pages | Un template sirve para 3 idiomas — no se duplica código |
| Islands solo donde hay interactividad | La web es rápida porque manda poco JavaScript |
| Schemas shared | metaSchema y heroSchema definidos una vez, usados en todos los schemas |
Reglas YAML que más usarás
YAML solo acepta espacios, no tabs. Los strings con
: o # necesitan comillas simples.titulo: 'Mi título' # string — con comillas si tiene : o #
precio: 42 # número — sin comillas
visible: true # booleano
hero: # objeto — campos indentados 2 espacios
heading: 'Hola'
sub: 'Descripción'
amenidades: # array de strings
- 'Wifi'
- 'Parking'
stats: # array de objetos — guión en primer campo de cada item
- num: '75%'
label: 'ocupación media'
- num: '4.8★'
label: 'valoración'
Ejemplos basados en el proyecto real · watan-astro · los conceptos son genéricos de Astro
15 capas, 3 grupos. Datos (qué hay), Renderizado (cómo se muestra), Soporte (cómo funciona todo junto).
Grupo A · Datos — dónde vive la información
Capa 1 · Contenido
Ficheros .md
src/content/es/ · en/ · ar/
Puro contenido en YAML (frontmatter). Un .md por página por idioma. Sin HTML ni lógica — solo datos estructurados.
Existe para que cambiar un texto no requiera tocar código. El .md es lo único que cambia entre la versión española e inglesa de la misma página — el template es idéntico.
src/content/ es/vacation-rental.md ← textos en español en/vacation-rental.md ← textos en inglés ar/vacation-rental.md ← textos en árabe
Cuando cambias un texto, añades una estadística, actualizas un testimonio o añades un caso de éxito.
Capa 2 · Validación
Schemas .schema.ts
src/schemas/
Contratos Zod. Cada campo del .md tiene su pareja aquí con su tipo y si es obligatorio.
z.strictObject rechaza también los campos que no están definidos.Existe porque los .md los edita gente que no es developer. Sin schema, un error tipográfico en el frontmatter llegaría a producción sin que nadie lo detecte. El schema lo atrapa en build, antes de publicar nada.
z.string() // obligatorio — falta → error
z.string().optional() // puede no estar en el .md
z.string().default('x') // si falta, usa 'x'
z.array(z.strictObject({ num: z.string() }))
z.coerce.number() // convierte '42' → 42
Cuando añades un campo nuevo al .md (primero schema, luego frontmatter), o cuando eliminas uno que ya no necesitas.
Capa 3 · Registro
content.config.ts
src/content.config.ts
El índice central de todo el contenido. Le dice a Astro: "busca estos ficheros con este patrón, valídalos con este schema, y llama a esta colección con este nombre".
Existe para que
getEntry('vacationRentalPages', ...) funcione desde cualquier template. Sin este registro, Astro no sabe qué ficheros existen ni cómo tratarlos. El patrón **/vacation-rental.md recoge los tres idiomas con una sola línea.const vacationRentalPages = defineCollection({
loader: glob({ pattern: '**/vacation-rental.md', base }),
schema: vacationRentalSchema, // ← valida con Zod
});
export const collections = { vacationRentalPages };
Solo cuando creas un tipo de página completamente nuevo (nuevo .md con estructura propia).
Capa 4 · Traducciones UI
i18n/
src/i18n/
Textos de interfaz que son iguales en todas las páginas: items del nav, labels de botones, textos del footer. No pertenecen a ninguna página concreta.
Existe para separar dos tipos de texto: el contenido (varía por página, va en .md) y la interfaz (igual en todas las páginas, va aquí). Sin esta capa, habría que poner "Ver más" en el frontmatter de cada .md de cada idioma — duplicación masiva.
export const nav = {
vacation: { en: 'Vacation Rental', es: 'Alquiler vacacional', ar: '...' },
};
// La función t() devuelve el texto del locale activo:
const label = t(nav.vacation, locale); // → 'Alquiler vacacional'
Cuando añades un texto de interfaz nuevo (un botón, un label) o corriges una traducción fija.
Grupo B · Renderizado — cómo los datos se convierten en HTML
Capa 5 · Maquetación
Templates .astro
src/templates/
El cerebro de cada página. Carga los datos del .md con
getEntry(), los recibe ya validados y tipados por el schema, y compone los componentes visuales en el orden correcto.Existe para que la maquetación viva en un solo sitio. Sin templates, la página de alquiler vacacional existiría 3 veces (es/en/ar) con código idéntico. Si cambias el orden de dos secciones, lo cambias en los 3 ficheros, y puedes cometer un error en uno de ellos.
const page = await getEntry('vacationRentalPages', `${locale}/vacation-rental`);
if (!page) throw new Error(`Entry no encontrada: ${locale}`);
const d = page.data; // TypeScript conoce el tipo exacto gracias al schema
// Renderizado condicional: solo si el campo existe en el .md
{d.stats && <StatsBar stats={d.stats} />}
Cuando cambias el layout de una página, añades una nueva sección, o reordenas componentes.
Capa 6 · URLs
Pages .astro
src/pages/
Cada fichero en
src/pages/ es una URL del sitio. La ruta del fichero es la URL: pages/es/contacto.astro → /es/contacto. Solo 3 líneas de código — delegan todo al template.Existe para separar "qué URL existe" de "cómo se ve esa URL". El template no sabe ni le importa en qué URL vive — es reutilizable. La page es el pegamento mínimo entre una URL y su template.
// pages/es/vacation-rental.astro import VacationRentalPage from '../../templates/VacationRentalPage.astro'; <VacationRentalPage locale="es" /> // pages/en/vacation-rental.astro — idéntico salvo el locale <VacationRentalPage locale="en" />
Solo cuando creas una URL nueva. Crea siempre los 3 ficheros (es/en/ar) con el mismo template, locale diferente.
Capa 7 · Componentes
Components .astro
src/components/
Los "ladrillos" del HTML. Cada componente hace una cosa concreta y recibe sus datos como props. No tienen JavaScript — generan HTML estático.
<slot /> permite pasar contenido HTML arbitrario dentro.Existe para no repetir el mismo bloque HTML en varios templates.
HeroSection se usa en 5 páginas distintas — si cambias el layout del hero, lo cambias en un fichero y todas las páginas se actualizan.// Definición del componente
interface Props { heading: string; sub?: string; }
const { heading, sub } = Astro.props;
// Uso desde el template (pasa datos del .md directamente)
<HeroSection heading={d.hero.heading} sub={d.hero.sub} />
Cuando creates un bloque visual nuevo que se usará en más de un sitio, o cuando modificas el diseño de un bloque existente.
Capa 8 · Esqueleto
Layout BaseLayout.astro
src/layouts/
La envoltura que comparten todas las páginas: el
<html>, el <head> con metatags SEO, el Nav y el Footer. El contenido específico de cada template entra por <slot />.Existe para que el
<head>, el Nav y el Footer estén definidos exactamente en un sitio. Sin layout, cada template tendría que repetir todo el esqueleto HTML — y si añades una etiqueta meta, la tienes que añadir en 10 templates a la vez.<BaseLayout title={d.meta.title} locale={locale}>
<!-- todo lo que va aquí → entra en <slot /> del layout -->
<HeroSection heading={d.hero.heading} />
<StatsBar stats={d.stats} />
</BaseLayout>
Rara vez. Solo si cambias el nav, el footer, los metatags globales, o añades un script/font que necesita toda la web.
Capa 9 · Islands
Islands .tsx
src/islands/
Componentes React para partes interactivas: acordeones, sliders, carruseles, mapas. Solo estos ficheros cargan JavaScript en el browser. El resto del sitio es HTML puro.
Astro genera HTML estático por defecto (0 KB de JS). Las islands son las "islas de interactividad" dentro de ese océano estático. Si meteras todo en React, el browser descargaría cientos de KB de JS para una web mayoritariamente de contenido. Con islands, solo los trozos que necesitan interactividad tienen JS.
<FAQAccordionList items={d.faq.items} client:visible />
// client:load → JS carga al cargar la página
// client:visible → JS carga cuando entra en pantalla ← más eficiente
// client:idle → JS carga cuando el browser está libre
Cuando necesitas que algo reaccione a clicks, inputs o gestos del usuario. Si no cambia al interactuar, usa un componente .astro normal.
Grupo C · Soporte — infraestructura que hace funcionar el resto
Capa 10 · Config Astro
astro.config.ts
raíz/
El cerebro de Astro. Define los idiomas del sitio, activa las integrations (React, sitemap), y configura Vite y Tailwind. Sin este fichero, los .tsx no funcionarían y el i18n no existiría.
Existe porque Astro por defecto no sabe que el proyecto tiene múltiples idiomas, ni que quieres usar React en los islands, ni que usas Tailwind. Cada capacidad extra necesita estar declarada aquí. Si añades una integration y no la registras, simplemente no funciona.
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'ar'],
routing: { prefixDefaultLocale: true }, // /en/... no se omite
},
integrations: [react(), sitemap()], // activa soporte .tsx
vite: { plugins: [tailwindcss()] }
Cuando añades un idioma nuevo, integras una librería nueva (React, Partytown, etc.), o cambias la URL base del sitio.
Capa 11 · Estilos
Tailwind CSS v4 + Design Tokens
src/styles/global.css
Dos capas de variables CSS: primitivos (qué color es:
navy-400) y semánticos (para qué sirve: surface, action). Los componentes usan siempre semánticos.La capa semántica existe para que cambiar la paleta de color sea editar un fichero, no buscar y reemplazar en 50 componentes. Si mañana
--color-action pasa de verde a azul, cambias una línea en global.css y toda la web se actualiza. Si hubieras usado el primitivo directamente (#a8c065), tendrías que encontrar y cambiar cada instancia.--color-surface: white; // semántico ← usa esto en componentes --color-action: #a8c065; // semántico ← usa esto en componentes --color-navy-400: #161f40; // primitivo ← solo para definir semánticos // En HTML (Tailwind v4): <p class="text-text bg-surface"> ← correcto <p class="text-navy-400"> ← evitar
Cuando añades un token de color nuevo, creas una clase de utilidad global, o ajustas la paleta del sitio.
Capa 12 · Datos estáticos
src/data/
src/data/
Datos estructurados que no encajan en el frontmatter de un .md. Typically: galerías de imágenes con decenas de entradas, listas de configuración, mapeos. TypeScript puro, con tipado completo.
Existe porque no todo dato pertenece al .md. Una galería de 30 fotos con rutas "before/after" haría el frontmatter ilegible y frágil.
src/data/ es para datos que son TypeScript, no YAML — puedes exportar funciones, hacer lógica, y los tipos son exactos sin schema.// Record<string, T[]> = objeto con claves string y valores T[]
export const successCaseGalleries: Record<string, GalleryItem[]> = {
'numancia-sagunto': [
{ before: 'antes/01-bano.jpeg', after: 'despues/01-bano.jpeg' },
{ before: 'antes/02-salon.jpeg', after: 'despues/02-salon.jpeg' },
],
};
Cuando tienes datos demasiado grandes o complejos para el frontmatter, o cuando los datos son compartidos entre páginas y no pertenecen a ninguna en particular.
Capa 13 · Utils
src/utils/
src/utils/
Funciones de utilidad reutilizables. No son datos, no son componentes — son lógica pura que se repite en varios sitios.
image-resolver.ts y success-cases.ts son los dos utils principales.resolveProjectImage existe porque Astro no puede optimizar una imagen si le pasas el path como string. Necesita un objeto ImageMetadata que solo obtiene cargando el fichero en build time con import.meta.glob. Sin este resolvedor, las imágenes no se redimensionarían ni se convertirían a WebP.// import.meta.glob precarga TODAS las imágenes en build time
const imageModules = import.meta.glob('/src/assets/**/*.{jpg,png}', { eager: true });
// Convierte un path string del .md → ImageMetadata que Astro puede optimizar
export function resolveProjectImage(path?: string): ImageMetadata | null {
if (!path) return null;
return imageModules[`/src/assets/projects/${path}`]?.default ?? null;
}
Cuando tienes lógica que se usa en más de un template o componente. Si solo la usa un sitio, déjala ahí — no abstraigas prematuramente.
Capa 14 · Rutas dinámicas
[id].astro + getStaticPaths
src/pages/es/vacation-rental/[id].astro
Para URLs que dependen de datos:
/es/vacation-rental/airbnb-valencia. El nombre [id] indica que ese segmento es variable. getStaticPaths() dice a Astro qué valores concretos puede tomar.Sin rutas dinámicas, tendrías que crear un fichero
airbnb-valencia.astro, numancia-sagunto.astro, etc. por cada apartamento — y si añades un caso nuevo al .md, tendrías que crear el fichero a mano. Con [id].astro y getStaticPaths(), Astro genera todas las páginas automáticamente leyendo el .md en build time.export async function getStaticPaths() {
const page = await getEntry('vacationRentalPages', 'es/vacation-rental');
return (page?.data.cases ?? []).map((item) => ({
params: { id: item.id }, // → /es/vacation-rental/airbnb-valencia
props: { caseItem: item }, // → disponible en Astro.props
}));
}
const { caseItem } = Astro.props; // TypeScript lo tipea correctamente
Cuando las URLs se generan a partir de datos (casos, productos, artículos) y no quieres crear un fichero por cada uno.
Capa 15 · API
Endpoints de servidor
src/pages/api/
Funciones que responden a peticiones HTTP (POST, GET...). El formulario de contacto envía datos a
/api/send-email, que los valida y los reenvía por SMTP. Las credenciales viven en .env, nunca en el código.Existe porque enviar un email desde el browser expondría las credenciales SMTP a cualquiera que inspeccione el código fuente. El endpoint corre en el servidor: recibe los datos del formulario, valida que sean correctos, y usa las variables de entorno para enviar el email sin que el browser las vea jamás.
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
if (!body.name?.trim()) // ?. = optional chaining: no falla si name es null
return new Response(JSON.stringify({ error: 'Name required' }), { status: 400 });
const smtpPass = import.meta.env.SMTP_PASS; // ← del .env, nunca en git
await transporter.sendMail({ ... });
return new Response(JSON.stringify({ success: true }), { status: 200 });
};
Cuando necesitas lógica de servidor: enviar emails, procesar pagos, llamar a una API con credenciales privadas, o guardar datos.
Resumen de tipos de fichero
| Extensión | Dónde vive | Para qué |
|---|---|---|
| .md | src/content/ | Contenido: textos, datos en YAML frontmatter |
| .schema.ts | src/schemas/ | Validar la forma de los datos con Zod |
| content.config.ts | src/ | Registrar colecciones para Astro |
| .astro (layout) | src/layouts/ | Esqueleto HTML de todas las páginas |
| .astro (template) | src/templates/ | Maquetación completa de una página |
| .astro (page) | src/pages/ | Define la URL, delega al template |
| .astro (component) | src/components/ | Bloque reutilizable de UI estático |
| .tsx (island) | src/islands/ | Componente React con interactividad |
| .ts (i18n) | src/i18n/ | Traducciones de textos de interfaz fijos |
Ejemplos basados en el proyecto real · watan-astro · los conceptos son genéricos de Astro
React solo vive en
src/islands/*.tsx — los componentes que necesitan responder a acciones del usuario. Todo lo demás es Astro puro (HTML estático, sin React).JSX vs HTML — diferencias
HTML normal
class="tarjeta"className="tarjeta"
for="campo"htmlFor="campo"
<br><br />
<!-- comentario -->{/* comentario */}
onclick="fn()"onClick={fn}
style="color: red"style={{ color: 'red' }}
Expresiones en JSX
{variable}inserta el valor
{cond && <X />}renderiza X solo si cond es truthy
{a ? <X/> : <Y/>}ternario (uno u otro)
{arr.map(...)}itera y renderiza lista
Hooks — cuándo usar cada uno
useState
Cuando necesitas datos que cambian y que React redibuje al cambiar. Siempre usa el setter, nunca modifiques la variable directamente.
const [expanded, setExpanded] = useState<Record<number, boolean>>({});
// ↑ valor actual ↑ setter para cambiarlo ↑ valor inicial
// Al hacer click en el FAQ número i — forma de función para estado que depende de estado anterior
onClick={() => setExpanded(prev => ({ ...prev, [i]: !prev[i] }))}
// ↑ spread: copia todos los demás, cambia solo [i]
// MAL — React no redibuja
expanded[i] = true;
// BIEN — React redibuja
setExpanded(prev => ({ ...prev, [i]: true }));
useRef
Cuando necesitas acceso directo a un elemento del DOM (medir su altura, hacer .focus(), etc.) sin que el componente se redibuje.
// Un ref por cada respuesta de FAQ — para animar la altura
const answerRefs = useRef<(HTMLDivElement | null)[]>([]);
// Se conecta al elemento real del DOM
<div ref={(el) => { answerRefs.current[i] = el; }}>
// Luego en useLayoutEffect se mide y anima:
const el = answerRefs.current[i];
el.style.maxHeight = expanded[i] ? `${el.scrollHeight}px` : '0';
// ↑ scrollHeight = altura real aunque esté oculto
useLayoutEffect
Como useEffect pero síncrono — corre antes de que el navegador pinte. Úsalo cuando necesitas medir o modificar el DOM sin que el usuario vea un parpadeo.
useLayoutEffect(() => {
items.forEach((_, i) => {
const el = answerRefs.current[i];
if (!el) return;
// Cambia maxHeight ANTES de que el navegador pinte → sin parpadeo
el.style.maxHeight = expanded[i] ? `${el.scrollHeight}px` : '0';
});
}, [expanded, items]); // ← array de dependencias: re-ejecuta cuando cambian
useEffect
Efectos después del paint: fetch de datos, suscripciones a eventos, inicializar librerías externas (como Leaflet) que necesitan el DOM real.
useEffect(() => {
// Aquí el DOM ya existe — Leaflet puede inicializarse
const map = L.map(mapRef.current).setView([39.47, -0.37], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// Función de limpieza — corre al desmontar el componente
return () => { map.remove(); };
}, []); // [] vacío → solo corre una vez al montar
useId
Genera un ID único y estable por instancia. Necesario para atributos
aria-controls / id de accesibilidad — evita colisiones si hay dos FAQs en la misma página.const uid = useId().replace(/:/g, ''); // uid → 'r0' (único por instancia)
<button aria-controls={`${uid}-answer-${i}`} aria-expanded={expanded[i] ? 'true' : 'false'}>
{item.q}
</button>
<div id={`${uid}-answer-${i}`}>{item.a}</div>
Cuándo usar isla vs componente .astro
| ¿Qué necesitas? | Usa |
|---|---|
| Reaccionar a clicks, inputs, cambios del usuario | Island .tsx con useState / useRef |
| Medir el DOM y animar antes de que el usuario lo vea | Island .tsx con useLayoutEffect + useRef |
| Inicializar una librería externa (Leaflet, charts) | Island .tsx con useEffect |
| HTML que no cambia — texto, imagen, layout | Componente .astro (sin JS) |
Islands de este proyecto
| Fichero | Hook principal | Para qué |
|---|---|---|
| FAQAccordionList.tsx | useState + useLayoutEffect + useRef + useId | Acordeón de preguntas con animación de altura |
| BeforeAfterSlider.tsx | useState + useRef | Slider de comparación antes/después |
| SuccessCaseCarousel.tsx | useState | Carrusel de casos de éxito con filtros |
| ProgressStepper.tsx | useState | Stepper de pasos con navegación |
| ZoneMapIsland.tsx | useEffect + useRef | Mapa interactivo con Leaflet — init tras render |
Manejadores de eventos — patrón importante
Diferencia clave:
onClick={fn} pasa la función; onClick={fn()} la ejecuta ahora (en el render). Siempre usa arrow function: onClick={() => setCuenta(n + 1)}// Click
<button onClick={() => setCuenta(cuenta + 1)}>Click</button>
// Input — e.target.value es el texto que escribió el usuario
<input onChange={(e) => setTexto(e.target.value)} />
// Submit — evitar recarga de página
<form onSubmit={(e) => { e.preventDefault(); enviarFormulario(); }}>
// Hover
<div onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}>
React · Guía de aprendizaje
Fundamentos · Estado & eventos · Gestión del estado · Hooks · Pensar en React · Basado en react.dev/learn
La UI en React es un árbol de componentes. Cada componente es una función JS que devuelve JSX. Los datos fluyen de padre a hijo mediante props. Nada más — el resto es consecuencia de este modelo.
Componentes
Concepto base
¿Qué es un componente?
Una función JavaScript que empieza en PascalCase y devuelve JSX. React la convierte en un elemento de la UI reutilizable, anidable y que puede recibir datos.
La mayúscula no es convención — es obligatoria. React usa eso para distinguir componentes (
<Boton>) de tags HTML nativos (<button>). Un componente en minúscula se trata como HTML y no funciona.// Componente correcto — PascalCase, devuelve JSX
export default function Tarjeta() {
return <div className="tarjeta">Hola</div>;
}
// Reutilizable: úsalo tantas veces como quieras
<Tarjeta />
<Tarjeta />
<Tarjeta />
Un componente solo puede devolver UN elemento raíz. Si necesitas devolver varios, envuélvelos en
<>...</> (Fragment) — no añade nodos al DOM.JSX
JSX no es HTML
JSX parece HTML pero es JavaScript disfrazado. El compilador lo transforma en
React.createElement(). Por eso sigue las reglas de JS, no de HTML.Dado que JSX es JS, las palabras reservadas de JS (
class, for) están prohibidas como atributos — de ahí className y htmlFor. Todos los tags deben cerrarse porque JS no tiene HTML implícito.// HTML → JSX
class="btn" → className="btn"
for="campo" → htmlFor="campo"
<br> → <br />
<img src="x.png"> → <img src="x.png" />
<!-- comentario --> → {/* comentario */}
onclick="fn()" → onClick={fn}
style="color:red" → style={{ color: 'red' }}
// Expresiones JS dentro de JSX — con llaves {}
<h1>{titulo}</h1>
<img src={usuario.foto} alt={usuario.nombre} />
<div className={activo ? 'btn-on' : 'btn-off'}>
Props
Props — datos de padre a hijo
Las props son los "argumentos" de un componente. Fluyen siempre de padre a hijo (unidireccional). Dentro del componente son de solo lectura — nunca se modifican.
Las props hacen los componentes reutilizables con datos diferentes. Sin props,
<Tarjeta> siempre mostraría lo mismo. Con props, <Tarjeta titulo="Airbnb" precio="500€" /> lo hace configurable. Cualquier intento de modificar una prop dentro del componente es un bug.// Definición — desestructuración + tipos + valor por defecto
function Tarjeta({ titulo, precio = 'Consultar', destacada = false }: {
titulo: string;
precio?: string;
destacada?: boolean;
}) {
return <div className={destacada ? 'card card--top' : 'card'}>
<h3>{titulo}</h3>
<p>{precio}</p>
</div>;
}
// Uso desde el padre
<Tarjeta titulo="Airbnb Valencia" precio="500€" destacada />
Props especiales
La prop children
children es la prop especial que recibe el contenido HTML que el padre pone entre las etiquetas del componente. Sirve para componentes "contenedor" que no saben qué tendrán dentro.// Componente contenedor genérico
function Panel({ titulo, children }: {
titulo: string;
children: React.ReactNode;
}) {
return (
<div className="panel">
<h2>{titulo}</h2>
<div className="panel-body">
{children} {/* ← aquí entra lo que el padre ponga */}
</div>
</div>
);
}
// Uso — lo que está entre las etiquetas va a children
<Panel titulo="Resultados">
<p>3 apartamentos encontrados</p>
<ListaApartamentos />
</Panel>
Pureza
Componente puro
Con las mismas props, siempre devuelve el mismo JSX. No modifica nada fuera de la función durante el render (sin efectos secundarios).
React puede re-renderizar un componente cuando quiera y tantas veces como quiera. Si el render tiene efectos secundarios (modificar variables externas, llamar APIs, leer la hora), el resultado será impredecible. En Strict Mode, React renderiza dos veces a propósito para detectar impurezas.
// MAL — efecto secundario en render
let visitas = 0;
function Impuro() {
visitas++; // ← modifica algo externo durante render
return <p>Visitas: {visitas}</p>;
}
// BIEN — solo depende de sus props
function Puro({ nombre }: { nombre: string }) {
const saludo = `Hola, ${nombre}`; // ← calculado a partir de props
return <p>{saludo}</p>; // mismas props → mismo JSX
}
Renderizado condicional
Mostrar u ocultar elementos
En JSX no hay
if directo — pero hay tres patrones equivalentes. Cada uno tiene su caso de uso.// 1. && — cuando solo hay un caso (mostrar o nada)
{tieneFotos && <Galeria fotos={fotos} />}
// ⚠ cuidado con 0: {count && <X />} pinta "0" si count=0
// Solución: {count > 0 && <X />}
// 2. Ternario — cuando hay dos casos (esto o aquello)
{cargando ? <Spinner /> : <Contenido />}
<button className={activo ? 'btn-on' : 'btn-off'}>
// 3. Variable precomputada — condición compleja
let contenido;
if (cargando) contenido = <Spinner />;
else if (error) contenido = <Error msg={error} />;
else contenido = <Contenido datos={datos} />;
return <div>{contenido}</div>;
Listas
Renderizar arrays con .map()
Para renderizar una lista de elementos, transforma el array en JSX con
.map(). Cada elemento necesita una prop key única y estable.key permite a React identificar qué elemento cambió, se añadió o se eliminó sin re-renderizar toda la lista. Sin key funciona, pero lento y con bugs en animaciones. Con el índice funciona si el orden nunca cambia — si puede cambiar (filtros, reordenación), usa el ID del dato.// Correcto — key es el ID del dato (estable y único)
{apartamentos.map((apt) => (
<Tarjeta key={apt.id} titulo={apt.titulo} precio={apt.precio} />
))}
// Aceptable solo si el orden NUNCA cambia
{items.map((item, i) => <li key={i}>{item}</li>)}
// Filtrar + mapear
{apartamentos
.filter(apt => apt.disponible)
.map(apt => <Tarjeta key={apt.id} {...apt} />)}
Nunca uses Math.random() como key — cambia en cada render y React re-monta el componente entero cada vez.
El estado es la memoria privada de un componente. Cuando cambia, React vuelve a ejecutar la función del componente (re-render) y actualiza el DOM con el nuevo resultado. Es el único mecanismo legítimo para cambiar la UI en respuesta a acciones del usuario.
useState — la base
Hook fundamental
useState
Devuelve un array de dos elementos: el valor actual y una función para actualizarlo (setter). Llama al setter → React re-renderiza el componente con el nuevo valor.
Las variables normales no funcionan para la UI porque cuando las cambias React no se entera y no redibuja nada. El setter de useState le avisa a React: "este componente tiene datos nuevos, vuelve a renderizarlo".
import { useState } from 'react';
function Contador() {
const [cuenta, setCuenta] = useState(0);
// ↑ valor ↑ setter ↑ valor inicial
return (
<div>
<p>Clicks: {cuenta}</p>
<button onClick={() => setCuenta(cuenta + 1)}>
+1
</button>
</div>
);
}
// MAL — React no se entera, no redibuja
let cuenta = 0;
cuenta++; // ← invisible para React
// BIEN — React redibuja con el nuevo valor
setCuenta(cuenta + 1);
Concepto clave
El estado como instantánea
El estado no es una variable que cambia — es una fotografía del valor en el momento del render. Cada render tiene su propia copia fija del estado.
Este es el concepto más contraintuitivo de React. Cuando llamas a
setCuenta(cuenta + 1) tres veces en el mismo handler, las tres lecturas de cuenta son el mismo valor (el de este render). No acumulas +3, acumulas +1.// Ejemplo — cuenta empieza en 0
function handler() {
setCuenta(cuenta + 1); // programa: "pon el estado a 0+1 = 1"
setCuenta(cuenta + 1); // programa: "pon el estado a 0+1 = 1"
setCuenta(cuenta + 1); // programa: "pon el estado a 0+1 = 1"
// Resultado: cuenta = 1, no 3
// 'cuenta' es siempre 0 durante todo este render
}
Para acumular múltiples cambios, usa la forma de función:
setCuenta(prev => prev + 1) — ve la sección siguiente.Actualizaciones en cola
La forma
prev =>Cuando el nuevo estado depende del estado anterior, pasa una función al setter. React la llama con el valor más reciente, no con el del render actual.
// Con función — acumula correctamente
function handler() {
setCuenta(prev => prev + 1); // prev=0 → 1
setCuenta(prev => prev + 1); // prev=1 → 2
setCuenta(prev => prev + 1); // prev=2 → 3
// Resultado: cuenta = 3 ✓
}
// Regla práctica:
// · El nuevo valor no depende del anterior → setCuenta(5)
// · El nuevo valor SÍ depende del anterior → setCuenta(prev => prev + 1)
// · Modificar objetos/arrays → setCuenta(prev => ({ ...prev, campo: nuevo }))
// Ejemplo real — toggle
setExpandido(prev => !prev);
// Ejemplo real — añadir a array
setItems(prev => [...prev, nuevoItem]);
Estado con objetos
Actualizar objetos — spread
El estado en React es inmutable. Para cambiar un campo de un objeto, crea un objeto nuevo con todos los campos del anterior más el campo modificado. Usa el spread operator.
Si mutas el objeto directamente (
usuario.nombre = 'Ana'), React compara la referencia del objeto — y es la misma que antes, así que decide que no hay cambios y no redibuja. Siempre crea un objeto nuevo.const [usuario, setUsuario] = useState({ nombre: 'Ana', edad: 25, ciudad: 'Valencia' });
// MAL — muta el objeto existente, React no redibuja
usuario.edad = 26;
setUsuario(usuario);
// BIEN — crea un objeto nuevo con spread
setUsuario({ ...usuario, edad: 26 });
// ↑ copia todos los campos, luego sobreescribe solo 'edad'
// Con objeto anidado — hay que hacer spread en cada nivel
const [form, setForm] = useState({ nombre: 'Ana', direccion: { ciudad: 'Valencia', cp: '46001' } });
setForm({ ...form, direccion: { ...form.direccion, ciudad: 'Madrid' } });
Estado con arrays
Actualizar arrays — sin mutar
Igual que con objetos: nunca mutaciones directas. Los métodos de array se dividen en permitidos (crean array nuevo) y prohibidos (mutan el original).
const [items, setItems] = useState(['Wifi', 'Parking', 'Piscina']);
// ✓ PERMITIDOS — crean array nuevo
setItems([...items, 'Gym']); // añadir al final
setItems(['Nuevo', ...items]); // añadir al principio
setItems(items.filter(i => i !== 'Parking')); // eliminar
setItems(items.map(i => i === 'Wifi' ? 'WiFi 6' : i)); // editar
setItems([...items].sort()); // sort (sobre copia)
// ✗ PROHIBIDOS — mutan el array original
items.push('Gym'); // ← React no redibuja
items.splice(1, 1); // ← React no redibuja
items.sort(); // ← muta in-place
items.reverse(); // ← muta in-place
Si el código de actualización se vuelve muy verboso con objetos anidados, considera la librería Immer — permite escribir mutaciones que internamente crea objetos nuevos.
Eventos
Manejadores de eventos
Los eventos se pasan como props en camelCase. La diferencia clave:
onClick={fn} pasa la función; onClick={fn()} la ejecuta durante el render — bug clásico.// onClick — click
<button onClick={() => setCuenta(cuenta + 1)}>+1</button>
// onClick con argumento
<button onClick={() => eliminar(item.id)}>Borrar</button>
// onChange — input (e.target.value = texto escrito)
<input onChange={(e) => setTexto(e.target.value)} value={texto} />
// onSubmit — formulario (siempre preventDefault)
<form onSubmit={(e) => {
e.preventDefault(); // evita recarga de página
enviar(texto);
}}>
// Pasar el manejador como prop (event handlers como props)
function Boton({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
<Boton onClick={() => setCuenta(0)}>Reset</Boton>
Cuándo el estado en un componente ya no es suficiente. Hay cuatro problemas frecuentes: estado mal estructurado, estado que necesitan varios componentes, prop drilling (pasar props 5 niveles), y lógica de actualización muy compleja. Cada uno tiene su solución.
Estructura del estado
Principio DRY
Qué no guardar en estado
Si un valor se puede calcular a partir del estado o las props, no es estado — es estado derivado. Calcularlo en el render, no guardarlo en
useState.Guardar estado derivado crea duplicación y fuente de verdad ambigua: si
nombre cambia, ¿se actualiza también nombreCompleto? Con la sincronización manual siempre hay un momento en que los dos están desincronizados.// MAL — nombreCompleto es derivado, no estado propio
const [nombre, setNombre] = useState('Ana');
const [apellido, setApellido] = useState('García');
const [nombreCompleto, setNombreCompleto] = useState('Ana García'); // ← innecesario
// BIEN — se calcula en el render, siempre sincronizado
const [nombre, setNombre] = useState('Ana');
const [apellido, setApellido] = useState('García');
const nombreCompleto = `${nombre} ${apellido}`; // ← calculado, no estado
// Otros ejemplos de estado derivado (no usar useState):
const totalItems = carrito.length; // derivado de carrito
const precioTotal = items.reduce(...); // derivado de items
const itemsFiltrados = items.filter(...); // derivado de items + filtro
Principio
Reglas de estructura del estado
Cinco reglas de la documentación oficial para tener un estado bien diseñado.
// 1. Agrupa lo que siempre cambia junto
// MAL:
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// BIEN:
const [pos, setPos] = useState({ x: 0, y: 0 });
// 2. Evita el estado contradictorio
// MAL: cargando=true y error="algo" al mismo tiempo → imposible en prod
// BIEN: un solo campo 'status': 'idle' | 'loading' | 'success' | 'error'
// 3. Evita duplicar datos del padre
// MAL: recibir usuario como prop y también guardarlo en estado propio
// BIEN: usar directamente la prop
// 4. Evita anidado profundo — aplana la estructura
// MAL: { pais: { ciudad: { barrio: { calle: '...' } } } }
// BIEN: { pais: 'ES', ciudad: 'Valencia', barrio: 'Ruzafa', calle: '...' }
Compartir estado
Levantar el estado al padre
Cuando dos componentes hermanos necesitan compartir datos o sincronizarse, el estado sube al ancestro común más cercano. Los hijos reciben el valor y los callbacks como props.
El flujo de datos en React es unidireccional (solo hacia abajo). Si dos hermanos comparten estado local propio, están desincronizados. La única forma de sincronizarlos es que el padre sea el propietario del estado y lo comparta hacia abajo.
// Dos paneles que se sincronizan — solo uno abierto a la vez
function Acordeon() {
const [abierto, setAbierto] = useState(null); // ← estado en el padre
return (
<>
<Panel titulo="Servicios" esActivo={abierto === 0} onToggle={() => setAbierto(0)} />
<Panel titulo="Precios" esActivo={abierto === 1} onToggle={() => setAbierto(1)} />
</>
);
}
// Panel no tiene estado propio — recibe todo del padre
function Panel({ titulo, esActivo, onToggle }) {
return (
<div>
<button onClick={onToggle}>{titulo}</button>
{esActivo && <div>Contenido...</div>}
</div>
);
}
Prop drilling
Context — evitar pasar props en cadena
Cuando una prop tiene que pasar por 4 componentes intermedios que no la usan, Context la hace disponible en cualquier descendiente sin pasarla manualmente.
Context no es un sustituto de las props — es para datos que son genuinamente "globales" al árbol: el locale activo, el usuario autenticado, el tema visual. Para datos que solo comparten 2-3 componentes cercanos, levanta el estado y pasa props normales.
// 1. Crear el contexto
const LocaleContext = createContext('es');
// 2. Proveedor — envuelve el árbol donde quieres que esté disponible
function App() {
const [locale, setLocale] = useState('es');
return (
<LocaleContext.Provider value={locale}>
<Pagina /> {/* Pagina → Seccion → Componente → ... */}
</LocaleContext.Provider>
);
}
// 3. Consumir en cualquier descendiente (sin pasar props)
function BotonIdioma() {
const locale = useContext(LocaleContext);
return <span>Idioma: {locale}</span>;
}
Lógica compleja
useReducer — consolidar actualizaciones
Sustituye a
useState cuando la lógica de actualización es compleja: múltiples sub-valores relacionados, actualizaciones que dependen del estado anterior, lógica repartida en muchos handlers.Con useState, si tienes 5 acciones distintas que modifican el mismo estado, la lógica queda dispersa en 5 handlers. Con useReducer, toda la lógica vive en una función pura (
reducer) que es fácil de testear, leer y compartir.// El reducer — función pura: (estadoActual, acción) → nuevoEstado
function reducer(state, action) {
switch (action.type) {
case 'añadir': return [...state, action.item];
case 'eliminar': return state.filter(i => i.id !== action.id);
case 'limpiar': return [];
default: throw new Error(`Acción desconocida: ${action.type}`);
}
}
function Lista() {
const [items, dispatch] = useReducer(reducer, []);
return (
<>
<button onClick={() => dispatch({ type: 'añadir', item: nuevoItem })}>
Añadir
</button>
<button onClick={() => dispatch({ type: 'limpiar' })}>
Limpiar todo
</button>
</>
);
}
Cuándo usar cada herramienta
| Herramienta | Cuándo | No usar cuando |
|---|---|---|
| useState | Estado simple e independiente de un componente. La mayoría de casos. | La lógica de actualización es muy compleja o hay muchas acciones. |
| useReducer | Estado con múltiples sub-valores relacionados. Muchas acciones distintas. Necesitas testear la lógica por separado. | Estado simple con 1-2 valores. No añade valor frente a useState. |
| Levantar estado | Dos componentes hermanos necesitan los mismos datos o sincronizarse. | Están muy lejos en el árbol — crear prop drilling de 5+ niveles. |
| Context | Datos globales al árbol: locale, usuario autenticado, tema. | Datos que solo necesitan 2-3 componentes cercanos — usa props normales. |
Los hooks son funciones que empiezan por
use. Solo pueden llamarse en el nivel superior de un componente o de otro hook — nunca dentro de condicionales, bucles o funciones anidadas. Esta regla garantiza que React siempre los llama en el mismo orden.Hooks de referencia y efectos
useRef referencia al DOM · valor sin re-render
Cuando necesitas acceso directo a un elemento del DOM (medir su altura, hacer .focus(), inicializar una librería) o guardar un valor que no debe provocar re-render cuando cambia.
A diferencia de useState, cambiar
ref.current no redibuja el componente. Útil para valores de "infraestructura" que el usuario no necesita ver: un timer ID, una instancia de mapa Leaflet, el valor anterior de un estado.import { useRef } from 'react';
// Caso 1 — referencia al DOM
function Formulario() {
const inputRef = useRef<HTMLInputElement>(null);
const enfocar = () => inputRef.current?.focus(); // ?. porque puede ser null inicialmente
return <>
<input ref={inputRef} type="text" />
<button onClick={enfocar}>Enfocar</button>
</>;
}
// Caso 2 — array de refs (uno por elemento de lista)
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
<div ref={(el) => { itemRefs.current[i] = el; }}>
// Caso 3 — valor que no provoca re-render (ej: ID de timer)
const timerId = useRef<number | null>(null);
timerId.current = setTimeout(() => { ... }, 1000);
clearTimeout(timerId.current);
useEffect efectos después del paint
Cuando necesitas sincronizar el componente con algo externo: llamar a una API, suscribirte a eventos del DOM, inicializar una librería que necesita el DOM real, conectar a un websocket.
Los efectos corren después de que React haya pintado en pantalla. El usuario ya ve el resultado antes de que el efecto corra. Si el efecto tarda mucho (un fetch lento), el componente ya está visible con el estado de carga — la UX no se bloquea.
import { useEffect, useState } from 'react';
function Perfil({ userId }: { userId: number }) {
const [datos, setDatos] = useState(null);
useEffect(() => {
// Este código corre DESPUÉS de que React pinte
fetch(`/api/usuario/${userId}`)
.then(r => r.json())
.then(d => setDatos(d));
// Función de limpieza — corre antes del próximo efecto o al desmontar
return () => {
// Cancelar la petición, desuscribirse, limpiar timers, etc.
};
}, [userId]); // ← dependencias: re-ejecuta cuando userId cambia
// Variantes del array de dependencias:
// [] → solo corre al montar (nunca más)
// [a, b] → corre al montar y cuando a o b cambian
// (sin array) → corre en cada render — casi siempre un bug
return <div>{datos?.nombre}</div>;
}
useLayoutEffect efectos antes del paint
Solo cuando necesitas medir o modificar el DOM antes de que el usuario lo vea. Si lo haces con useEffect, el usuario verá un parpadeo (el estado incorrecto un frame antes de corregirse).
Corre síncronamente después del commit de React pero antes de que el navegador pinte. Es más caro que useEffect porque bloquea el paint. Úsalo solo para animaciones y medidas del DOM que necesitan ser invisibles para el usuario.
import { useLayoutEffect, useRef } from 'react';
// Animar altura de un acordeón sin parpadeo
function Acordeon({ abierto, children }) {
const contenedorRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const el = contenedorRef.current;
if (!el) return;
// Cambia la altura ANTES de que el usuario vea el render
// → sin parpadeo, animación suave
el.style.maxHeight = abierto ? `${el.scrollHeight}px` : '0';
}, [abierto]); // re-ejecuta cada vez que cambia 'abierto'
return (
<div ref={contenedorRef} style={{ overflow: 'hidden', transition: 'max-height 0.3s' }}>
{children}
</div>
);
}
// Regla práctica:
// Empieza siempre con useEffect.
// Si ves un parpadeo visual, cambia a useLayoutEffect.
useId IDs únicos para accesibilidad
Cuando necesitas conectar elementos de formulario o ARIA con
id y htmlFor / aria-controls, y el componente puede instanciarse varias veces en la misma página.Si inventas IDs estáticos (
id="faq-1") y tienes dos instancias del componente FAQ en la misma página, los IDs se repiten — eso rompe la accesibilidad y los lectores de pantalla. useId garantiza un ID único por instancia, sea cual sea el número de instancias.import { useId } from 'react';
function CampoFormulario({ label, tipo = 'text' }) {
const id = useId(); // genera: ':r0:', ':r1:', etc. — único por instancia
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type={tipo} />
</div>
);
}
// Dos instancias — cada una tiene su ID único, no hay conflictos
<CampoFormulario label="Nombre" />
<CampoFormulario label="Email" tipo="email" />
// En acordeones — conecta botón con contenido (aria)
const uid = useId().replace(/:/g, '');
<button aria-controls={`${uid}-panel-${i}`} aria-expanded={abierto[i]}>
<div id={`${uid}-panel-${i}`}>
Custom hooks —
use... extraer lógica reutilizableCuando la misma lógica con hooks aparece en dos o más componentes. Un custom hook extrae esa lógica en una función reutilizable con su propio estado.
Los custom hooks no son una API especial de React — son simplemente funciones que empiezan por
use y pueden llamar a otros hooks. La convención del prefijo es obligatoria: React usa el nombre para aplicar las reglas de hooks (linters, herramientas).// Custom hook — extrae lógica de fetch reutilizable
function useFetch<T>(url: string) {
const [datos, setDatos] = useState<T | null>(null);
const [cargando, setCargando] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setCargando(true);
fetch(url)
.then(r => r.json())
.then(d => { setDatos(d); setCargando(false); })
.catch(e => { setError(e.message); setCargando(false); });
}, [url]);
return { datos, cargando, error };
}
// Uso — limpio, sin repetir la lógica de fetch
function Apartamentos() {
const { datos, cargando, error } = useFetch('/api/apartamentos');
if (cargando) return <Spinner />;
if (error) return <p>Error: {error}</p>;
return <Lista items={datos} />;
}
Resumen — cuándo usar cada hook
| Hook | Para qué | Provoca re-render |
|---|---|---|
| useState | Datos que cambian y que la UI debe reflejar | Sí — al llamar al setter |
| useReducer | Como useState pero con lógica compleja centralizada | Sí — al hacer dispatch |
| useRef | Referencia al DOM · Valores que no deben redibujar | No |
| useEffect | Sincronizar con sistemas externos, fetch, suscripciones | Indirectamente (si llama setState) |
| useLayoutEffect | Medir/modificar el DOM antes del paint, sin parpadeo | Indirectamente |
| useId | IDs únicos por instancia para accesibilidad | No |
| useContext | Leer datos de un Context sin prop drilling | Sí — cuando el Context cambia |
| useCallback | Memorizar una función para no recrearla en cada render | No |
| useMemo | Memorizar un valor calculado caro de computar | No |
El método oficial de la documentación de React para atacar cualquier UI nueva. 5 pasos en orden. La clave: construir primero sin estado, añadir estado solo al final.
Los 5 pasos
-
1
Separa la UI en una jerarquía de componentesDibuja cajas en el boceto. Cada caja que hace una sola cosa es un componente candidato (principio de responsabilidad única). Si un componente crece demasiado, se divide. El resultado es una jerarquía en árbol.
// Ejemplo: buscador de apartamentos App ├── BarraBusqueda ← input de texto + checkbox └── TablaApartamentos ← tabla con datos filtrados ├── FilaCategoria ← fila de encabezado (Playas, Centros...) └── FilaApartamento ← una fila por apartamento -
2
Construye una versión estática primeroImplementa los componentes con props y datos fijos — sin useState, sin eventos. Solo renderizado. Esto requiere escribir mucho código pero pensar poco (sin interactividad aún). El estado se añade en el paso 3.
// Versión estática — datos hardcodeados, sin estado function App() { return <TablaApartamentos apartamentos={DATOS_EJEMPLO} />; } function TablaApartamentos({ apartamentos }) { return ( <table> {apartamentos.map(apt => ( <FilaApartamento key={apt.id} apt={apt} /> ))} </table> ); } // Sin useState. Sin onClick. Solo JSX y props. -
3
Identifica el estado mínimo necesarioPregúntate por cada dato: ¿se puede calcular a partir de otra cosa? → no es estado. ¿Es siempre igual (constante)? → no es estado. ¿Viene de un padre como prop? → no es estado. Lo que queda es el estado real.
// Datos del ejemplo: // - Lista de apartamentos → prop (viene del padre) — NO es estado // - Texto de búsqueda → cambia, no se calcula → SÍ es estado // - Checkbox "solo disponibles" → cambia, no se calcula → SÍ es estado // - Lista filtrada → se calcula de las dos anteriores → NO es estado const [textoBusqueda, setTextoBusqueda] = useState(''); const [soloDisponibles, setSoloDisponibles] = useState(false); // Derivado — se calcula en render, no es estado const aptsFiltrados = apartamentos .filter(a => !soloDisponibles || a.disponible) .filter(a => a.nombre.toLowerCase().includes(textoBusqueda.toLowerCase())); -
4
Decide dónde vive cada estadoPara cada estado: ¿qué componentes lo necesitan? Sube hasta el ancestro común más cercano. Si no existe un componente apropiado, crea uno nuevo solo para contener ese estado.
// textoBusqueda y soloDisponibles los necesitan: // - BarraBusqueda (para mostrar el valor actual del input) // - TablaApartamentos (para filtrar) // // Ancestro común: App → el estado vive en App function App() { const [textoBusqueda, setTextoBusqueda] = useState(''); const [soloDisponibles, setSoloDisponibles] = useState(false); return ( <> <BarraBusqueda texto={textoBusqueda} soloDisp={soloDisponibles} /> <TablaApartamentos texto={textoBusqueda} soloDisp={soloDisponibles} /> </> ); } -
5
Añade el flujo de datos inversoLos hijos no pueden modificar el estado del padre directamente. El padre les pasa callbacks como props. El hijo llama al callback cuando el usuario interactúa — el estado cambia en el padre y baja de nuevo al hijo.
// El padre pasa los setters como callbacks function App() { const [texto, setTexto] = useState(''); const [soloDisp, setSoloDisp] = useState(false); return ( <> <BarraBusqueda texto={texto} soloDisp={soloDisp} onTexto={setTexto} // ← callback hacia arriba onSoloDisp={setSoloDisp} // ← callback hacia arriba /> <TablaApartamentos texto={texto} soloDisp={soloDisp} /> </> ); } // El hijo llama al callback — no modifica estado directamente function BarraBusqueda({ texto, soloDisp, onTexto, onSoloDisp }) { return ( <> <input value={texto} onChange={e => onTexto(e.target.value)} /> <input type="checkbox" checked={soloDisp} onChange={e => onSoloDisp(e.target.checked)} /> </> ); }
Resumen del flujo completo
| Paso | Qué haces | Sin estado todavía |
|---|---|---|
| 1 · Jerarquía | Dibuja cajas en el boceto, nombra los componentes | Sí |
| 2 · Estático | Implementa con props y datos fijos, sin eventos | Sí |
| 3 · Estado mínimo | Identifica qué cambia que NO es calculable ni prop | No (primer useState) |
| 4 · Dónde vive | Sube el estado hasta el ancestro común que lo necesite | No |
| 5 · Flujo inverso | Pasa callbacks de padre a hijo, hijo los llama | No |
La trampa más común: añadir estado demasiado pronto. Los pasos 1 y 2 son estáticos a propósito. Entender primero la estructura de componentes y el flujo de datos hace que decidir el estado en el paso 3 sea obvio.