Usar JavaScript vanilla no obliga a escribir código desordenado. El problema aparece cuando se confunde “sin framework” con “sin arquitectura”: selectores repartidos por toda la página, eventos duplicados, estado escondido en el DOM y estilos que solo funcionan por accidente.

Una arquitectura basada en componentes puede existir perfectamente sin React, Vue o Svelte. La diferencia es que tienes que ser más explícito con los contratos: qué recibe cada pieza, qué evento emite, qué estado controla y qué parte del DOM le pertenece.

Puntos clave

  • Vanilla JS funciona bien cuando el estado es local, las interacciones son acotadas y el coste de framework no compensa.
  • Un componente no es solo un bloque HTML: necesita contrato, inicialización, eventos, accesibilidad y limpieza.
  • La arquitectura debe permitir migrar después sin reescribir todo desde cero.
  • El rendimiento mejora cuando cada componente carga solo lo que necesita.
  • Si aparece mucho estado compartido o rutas complejas, conviene evaluar un framework.

Qué entiendo por componente en este contexto

Un componente vanilla es una unidad de interfaz con responsabilidad concreta. Puede ser una tarjeta interactiva, un buscador, un acordeón, un modal, un filtro, un menú o una tabla editable. Lo importante no es el tamaño, sino el límite.

Un buen componente responde a estas preguntas:

PreguntaEjemplo
¿Qué DOM controla?Solo el nodo raíz recibido en init()
¿Qué datos necesita?Props desde data-*, JSON embebido o API
¿Qué eventos emite?filter:change, modal:open, form:valid
¿Qué estado mantiene?Abierto/cerrado, valor seleccionado, página actual
¿Cómo se destruye?Limpieza de listeners, timers y observers
Sistema de componentes vanilla con módulos independientes y límites claros
Una arquitectura de componentes también puede existir sin JSX ni framework pesado.

Estructura de carpetas que escala sin framework

Una estructura simple puede ser suficiente:

src/
  components/
    accordion/
      accordion.js
      accordion.css
      README.md
    lead-filter/
      lead-filter.js
      lead-filter.css
      contract.md
  lib/
    dom.js
    events.js
    fetch-json.js
  pages/
    blog.js
    landing.js

El objetivo no es imitar un framework, sino evitar acoplamientos invisibles. Cada componente debería exponer una función de inicialización y, si procede, una función de destrucción:

export function initAccordion(root) {
  const buttons = [...root.querySelectorAll("[data-accordion-trigger]")];

  function onClick(event) {
    const panel = event.currentTarget.nextElementSibling;
    const isOpen = event.currentTarget.getAttribute("aria-expanded") === "true";
    event.currentTarget.setAttribute("aria-expanded", String(!isOpen));
    panel.hidden = isOpen;
  }

  buttons.forEach((button) => button.addEventListener("click", onClick));

  return () => {
    buttons.forEach((button) => button.removeEventListener("click", onClick));
  };
}

Ese patrón obliga a pensar en ownership. El componente no busca por toda la página, no muta nodos ajenos y no deja listeners colgados.

Contratos de UI entre componentes con eventos y límites bien definidos
Los contratos claros entre componentes reducen acoplamiento y facilitan evolución técnica.

Contratos antes que convenciones frágiles

Cuando un componente falla en producción, casi siempre el problema está en un contrato implícito. Un diseñador cambia una clase, un editor mueve un bloque, un endpoint devuelve un campo distinto y el componente deja de funcionar.

Para evitarlo, cada componente debería documentar:

  • selector raíz;
  • atributos data-* que consume;
  • eventos que escucha y emite;
  • requisitos de accesibilidad;
  • dependencias de CSS o tokens;
  • errores esperados;
  • ejemplo mínimo de HTML.

Ejemplo de contrato:

<section data-component="lead-filter">
  <button data-filter="enterprise">Empresas</button>
  <button data-filter="agency">Agencias</button>
  <div data-results></div>
</section>
import { initLeadFilter } from "./components/lead-filter/lead-filter.js";

document
  .querySelectorAll('[data-component="lead-filter"]')
  .forEach((root) => initLeadFilter(root));

Estado: local por defecto, compartido solo cuando aporta

Vanilla se complica cuando el estado compartido se improvisa. Antes de crear un store global, conviene preguntar si el estado realmente lo necesita.

EstadoDónde viviríaMotivo
Modal abierto/cerradoComponenteNo afecta al resto
Filtro de una listaComponente o URLPuede necesitar deep-linking
Usuario autenticadoCapa de aplicaciónLo usan varias piezas
Carrito o checkoutStore claro o frameworkEstado crítico y compartido

Si un proyecto empieza a necesitar sincronización constante entre componentes, rutas dinámicas y actualizaciones reactivas, quizá la pregunta ya no sea “cómo hacerlo en vanilla”, sino “qué framework reduce riesgo”.

Rendimiento y carga progresiva

Una ventaja real de vanilla es poder cargar poco JavaScript. Pero esa ventaja se pierde si cada página importa un bundle común con interacciones que no usa. En Astro, por ejemplo, la filosofía de islas ayuda precisamente a enviar menos JavaScript al cliente cuando la página puede ser estática.

Un criterio práctico:

  1. HTML funcional primero.
  2. CSS sin dependencia de JavaScript siempre que sea posible.
  3. JavaScript solo para mejorar interacción.
  4. Importación por página o por componente.
  5. Medición de peso, INP y errores de consola.

Cuándo migrar a un framework

Migrar tiene sentido cuando el coste de mantener la arquitectura manual supera el coste del framework. Señales típicas:

  • muchas piezas dependen del mismo estado;
  • hay rutas, permisos y vistas dinámicas complejas;
  • el equipo necesita un patrón común de testing;
  • hay duplicación de lógica entre componentes;
  • las interacciones dependen de datos en tiempo real;
  • los cambios de producto rompen piezas no relacionadas.

La migración no tiene por qué ser total. Se puede aislar primero la zona con más complejidad y mantener vanilla en páginas estáticas o componentes simples.

Lecturas relacionadas

Conclusión

Vanilla JS no es una excusa para renunciar a la arquitectura. Al contrario: obliga a ser más consciente de contratos, límites y coste de mantenimiento. Si el proyecto es simple, esa claridad puede ser una ventaja enorme. Si deja de serlo, la misma arquitectura te dará una ruta de migración menos traumática.

Preguntas frecuentes

¿Tiene sentido usar componentes sin React o Vue?
Sí, si el producto tiene interacciones acotadas, poca necesidad de estado global y prioridad en rendimiento. La clave es definir contratos, eventos y límites de responsabilidad.
¿Cuándo conviene migrar desde vanilla JS a un framework?
Conviene migrar cuando aparecen estado compartido complejo, rutas ricas, equipos grandes, reutilización intensa o pruebas que se vuelven difíciles con módulos sueltos.
¿Qué debe tener un componente vanilla mantenible?
Debe tener una API clara, estado local controlado, eventos documentados, accesibilidad, pruebas mínimas y estilos que no dependan de selectores frágiles.

Volver al Archivo