12k
All articles

5 Cosas para las que No Necesitas React

Cinco API nativas del navegador reemplazan componentes React comunes: dialog, Popover, Custom Elements, container queries y View Transitions.

OpenReplay Team
OpenReplay Team
5 Cosas para las que No Necesitas React

La plataforma del navegador ha incorporado reemplazos nativos, disponibles de forma generalizada, para varios primitivos de interfaz de usuario que antes requerían componentes de React o librerías de terceros: diálogos modales, popovers y tooltips, widgets reutilizables independientes del framework, layouts responsivos con conciencia del contenedor, y transiciones de vista animadas. Este artículo no es un argumento en contra de React — sigue siendo la herramienta adecuada para estado compartido complejo, flujos de trabajo con formularios extensos, y ecosistemas como Next.js y Remix. Es una lista de verificación de auditoría para mantenedores: cinco categorías de componentes que quizás ya tienes en una base de código React y que el navegador ahora puede manejar de forma nativa, sin añadir peso al bundle de JavaScript.

El público objetivo es el desarrollador React en activo cuyo modelo mental de “qué puede hacer el navegador” dejó de actualizarse en algún momento alrededor de 2020. React 19 es la versión estable actual, y varios de los patrones que su ecosistema resolvió son ahora parte de la plataforma. Cada sección a continuación nombra el reflejo de React, la API nativa que lo reemplaza, y la advertencia de accesibilidad específica que necesitas conocer antes de eliminar código.

Puntos Clave

  • El elemento HTML <dialog> con showModal() proporciona captura de foco nativa, cierre con la tecla Escape, y un pseudo-elemento ::backdrop — eliminando la mayoría de las razones para depender de una librería modal de React.
  • La Popover API renderiza elementos en la capa superior del navegador, lo que elimina toda la categoría de bugs de z-index y recorte por overflow: hidden que plagan los tooltips y dropdowns de React implementados manualmente.
  • Los Custom Elements con Shadow DOM permiten distribuir un único widget que funciona en cualquier framework o en HTML plano, sin necesidad de reimplementarlo por stack.
  • Las CSS Container Queries (@container) permiten que un componente responda al ancho de su elemento padre, reemplazando los hooks de ResizeObserver y el estado de React utilizado exclusivamente para decisiones de layout.
  • La View Transitions API (document.startViewTransition()) anima los cambios de estado del DOM de forma nativa, cubriendo muchos casos de uso que antes eran manejados por Framer Motion o react-transition-group.

Modales: usa el elemento <dialog> en lugar de una librería modal

Para los diálogos modales, el elemento HTML <dialog> nativo invocado mediante showModal() te proporciona captura de foco, contenido de fondo inerte, cierre con la tecla Escape, y estilos para el backdrop — comportamientos que un modal personalizado de React debe implementar manualmente y que con frecuencia implementa incorrectamente. El elemento <dialog> forma parte de Baseline; verifica la fecha exacta de disponibilidad en MDN antes de publicar documentación interna.

El patrón React. Los equipos suelen recurrir a react-modal, Radix Dialog, o un hook personalizado useModal respaldado por un portal. El patrón de hook generalmente combina createPortal, un useEffect que alterna document.body.style.overflow, y una trampa de foco escrita a mano. Las grabaciones de sesiones en producción de estas implementaciones frecuentemente muestran a usuarios que salen del modal con Tab hacia el contenido de fondo — un síntoma de lógica de trampa de foco incompleta.

La API nativa.

<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">¿Eliminar proyecto?</h2>
  <p>Esta acción no se puede deshacer.</p>
  <form method="dialog">
    <button value="cancel">Cancelar</button>
    <button value="confirm">Eliminar</button>
  </form>
</dialog>

<script>
  document.getElementById('confirm').showModal();
</script>

showModal() coloca el diálogo en la capa superior, captura el foco dentro de él, hace que el resto del documento sea inerte, y renderiza el pseudo-elemento ::backdrop que puedes estilizar con CSS. Un <form method="dialog"> cierra el diálogo y devuelve el value del botón pulsado a través de dialog.returnValue — sin necesidad de un event listener.

Advertencias. El problema de accesibilidad es que <dialog> no anuncia una etiqueta automáticamente. Necesitas aria-labelledby apuntando a un encabezado visible (o aria-label) para que los lectores de pantalla puedan identificarlo. Si el diálogo no es modal — abierto con show() en lugar de showModal() — no captura el foco, y puede que prefieras usar la Popover API en su lugar. React o una librería sigue siendo la mejor opción cuando necesitas lógica de apertura/cierre declarativa vinculada al estado y estrechamente acoplada a otros componentes, o animaciones que se ejecutan antes de que el diálogo se desmonte.

Popovers, tooltips y dropdowns: usa la Popover API

La Popover API renderiza elementos en la capa superior del navegador, lo que significa que un popover siempre aparece por encima del resto del contenido independientemente del contexto de apilamiento o del overflow: hidden de un ancestro. Esto elimina toda la categoría de conflictos de z-index y bugs de recorte que producen las implementaciones manuales de tooltips y dropdowns.

El patrón React. Floating UI, Radix Popover, y los primitivos de overlay de React-Aria son dependencias habituales. Gestionan el posicionamiento, el cierre al hacer clic fuera, y el renderizado en portales. Para un tooltip simple, esto supone importar una cantidad considerable de código.

La API nativa.

<button popovertarget="menu">Abrir menú</button>

<div id="menu" popover>
  <a href="/account">Cuenta</a>
  <a href="/logout">Cerrar sesión</a>
</div>

El atributo popover por sí solo — sin JavaScript — te proporciona un elemento que se activa mediante un botón popovertarget, se cierra al hacer clic fuera y con Escape, y se renderiza en la capa superior. El valor predeterminado popover="auto" habilita el cierre automático al hacer clic fuera (light-dismiss); popover="manual" lo deshabilita para los casos en los que deseas un control explícito. La Popover API es Baseline Newly Available; consulta la tabla de compatibilidad de MDN para conocer el estado actual.

Advertencias. El problema de accesibilidad es que, a diferencia de showModal() de <dialog>, la Popover API no gestiona el foco automáticamente. Si tu popover funciona como un menú, aún necesitas aplicar role="menu", gestionar el roving tabindex, y mover el foco al popover cuando se abre. Para el posicionamiento relativo al elemento que lo activa, también necesitas CSS Anchor Positioning, cuyo estado en Baseline es más limitado — verifica en MDN antes de confiar en él entre navegadores. Para menús complejos con submenús, patrones de navegación por teclado y typeahead, una librería como Radix o React-Aria sigue ahorrando trabajo real.

Widgets reutilizables: usa Custom Elements y Shadow DOM

Un Custom Element registrado con customElements.define() funciona en cualquier contexto HTML — React, Vue, Angular, Svelte, o un archivo HTML plano — sin necesidad de reimplementación. Combinado con Shadow DOM, proporciona encapsulación de estilos sin CSS Modules, CSS-in-JS, ni un paso de compilación. Los Custom Elements y Shadow DOM son Baseline Widely Available; verifica el año en MDN.

Los Web Components no han reemplazado a React en el desarrollo de aplicaciones convencionales. Lo que sí han reemplazado es la necesidad de distribuir el mismo widget cinco veces — una por framework — cuando mantienes un sistema de diseño o distribuyes un embed de terceros.

El patrón React. Un botón, badge o gráfico reutilizable envuelto en un componente React, publicado en npm, y reimplementado (o re-envuelto) para cualquier equipo que utilice un framework diferente.

La API nativa.

class CopyButton extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' }).innerHTML = `
      <style>button { padding: 6px 12px; }</style>
      <button><slot>Copiar</slot></button>
    `;
    this.shadowRoot.querySelector('button')
      .addEventListener('click', () => {
        navigator.clipboard.writeText(this.dataset.value ?? '');
      });
  }
}
customElements.define('copy-button', CopyButton);

Se usa como <copy-button data-value="hello">Copiar</copy-button> en cualquier HTML, incluyendo dentro de JSX. React 19 soporta custom elements directamente, incluyendo el paso de props de tipo objeto y la escucha de eventos personalizados.

Advertencias. El problema de accesibilidad es que el árbol de accesibilidad no atraviesa los límites del shadow DOM por defecto — las referencias de aria-labelledby y aria-describedby en el light DOM no pueden apuntar a IDs dentro de un shadow root, y viceversa. La especificación ARIA en HTML y la propuesta reference target (en desarrollo) abordan esto, pero los patrones prácticos actuales requieren o bien atributos ARIA explícitos en el elemento host, o bien attachInternals() con ElementInternals. React sigue siendo la mejor opción cuando un widget necesita integrarse estrechamente con el estado de la aplicación, compartir React Context, o usar Suspense.

Layout responsivo a nivel de componente: usa CSS Container Queries

Las CSS Container Queries (@container) permiten que un componente adapte su layout en función del ancho de su propio elemento padre, en lugar del viewport. Esto elimina el patrón del hook useResizeObserver, donde el estado de React rastrea las dimensiones del contenedor exclusivamente para controlar un className. Las Container Queries son Baseline Widely Available — verifica el año en MDN.

El patrón React. Un hook useResizeObserver (habitualmente de @react-hook/resize-observer o implementado a mano) conectado al estado del componente que intercambia una prop o className layout="compact". Cada cambio de tamaño desencadena un render de React, aunque el único consumidor sea CSS.

La API nativa.

.card-container {
  container-type: inline-size;
}

.card {
  display: grid;
  grid-template-columns: 1fr;
}

@container (min-width: 400px) {
  .card {
    grid-template-columns: 120px 1fr;
  }
}

Declara container-type: inline-size en el elemento padre, luego escribe reglas @container para el elemento hijo. El navegador gestiona la observación del cambio de tamaño de forma nativa. Sin JavaScript, sin re-renders, sin desajustes de hidratación.

El selector :has() complementa esto para el estilizado con conciencia del estado. Una regla como form:has(input:invalid) button[type="submit"] { opacity: 0.5 } expresa lo que antes requería useState y un patrón de input controlado. :has() es Baseline Widely Available — comprueba en MDN.

Advertencias. La consideración de accesibilidad es sutil pero real: las container queries pueden cambiar el layout de forma drástica sin modificar el orden del DOM, lo cual es positivo para los lectores de pantalla, pero significa que debes verificar que el orden de lectura coincide con el orden visual en cada breakpoint. Las container queries también introducen un comportamiento de contención que puede afectar al layout y posicionamiento de los elementos descendientes, así que prueba los componentes que dependen de posicionamiento relativo al viewport u otras suposiciones de layout. El estado de React sigue siendo necesario cuando la decisión de layout afecta a algo más que el estilizado — por ejemplo, cuando necesitas renderizar un árbol de componentes diferente, no solo reestilizar uno.

Transiciones animadas: usa la View Transitions API

La View Transitions API envuelve una actualización del DOM en una animación de fundido cruzado por defecto, con control total de CSS sobre la transición mediante pseudo-elementos ::view-transition-*. Para las transiciones dentro del mismo documento, cubre la mayoría de las animaciones de transición de ruta y de estado que antes requerían librerías de animación.

El patrón React. Framer Motion, react-transition-group, o wrappers AnimatePresence alrededor de los componentes de ruta. Funcionan, pero requieren que la animación sea expresable en el modelo de renderizado de React, lo cual resulta incómodo para transiciones que abarcan el desmontaje de un árbol y el montaje de otro.

La API nativa.

function navigate(url) {
  if (!document.startViewTransition) {
    updateDOM(url);
    return;
  }
  document.startViewTransition(() => updateDOM(url));
}

document.startViewTransition() recibe un callback que realiza la actualización del DOM. El navegador captura el estado anterior, ejecuta el callback, captura el estado posterior, y aplica un fundido cruzado entre ambos. Para animar un elemento específico a lo largo de la transición — por ejemplo, una miniatura que se expande en una vista de detalle — asigna el mismo view-transition-name en CSS a los elementos correspondientes. Las View Transitions para el mismo documento son Baseline Newly Available; las View Transitions entre documentos (para navegaciones MPA) tienen soporte más limitado — consulta la tabla de compatibilidad de MDN y el blog de WebKit para conocer el estado actual de Safari antes de confiar en el modo entre documentos.

Advertencias. El problema de accesibilidad es el movimiento: respeta prefers-reduced-motion envolviendo la transición en una media query o omitiendo la llamada por completo para los usuarios que lo desactivan. El fundido cruzado predeterminado es breve, pero sigue siendo una animación. Las librerías de React siguen siendo la mejor opción cuando necesitas física de resortes, transiciones controladas por gestos, o animaciones que se interrumpen y revierten a mitad de su ejecución — las view transitions son atómicas y no están diseñadas para eso.

Dónde React sigue siendo superior

Los cinco reemplazos anteriores se dirigen a categorías de componentes específicas. Para todo lo que se menciona a continuación, React sigue siendo la herramienta adecuada, y reemplazarlo con características de la plataforma costaría más de lo que ahorraría.

  • Estado compartido complejo entre componentes distantes. Cuando múltiples partes no relacionadas de la UI se suscriben al mismo estado en evolución con selectores derivados, librerías como Zustand, Jotai, o Redux Toolkit realizan un trabajo que la plataforma no hace. Los eventos personalizados en Web Components pueden transportar datos, pero no modelan estado derivado.
  • Flujos de trabajo con formularios extensos con validación entre campos y renderizado dinámico. El <form> nativo, la Constraint Validation API, y FormData gestionan el envío de formularios simples de forma limpia. Los asistentes de múltiples pasos, los campos condicionales que dependen de valores en otras partes del formulario, la validación del servidor combinada con la validación del cliente, y los arrays de campos siguen beneficiándose de React Hook Form o TanStack Form.
  • Renderizado y obtención de datos dirigidos por el servidor. Los React Server Components, el hook use() para datos asíncronos, y el modelo de SSR con streaming en Next.js y Remix resuelven problemas de hidratación, code-splitting, y coordinación de la obtención de datos que la plataforma no aborda directamente.
  • Madurez del ecosistema para routing y capas de datos. TanStack Router, TanStack Query, y el consolidado ecosistema de React Router proporcionan invalidación de caché, actualizaciones optimistas, y patrones de route-loader que requerirían un trabajo considerable para replicar con APIs nativas.
  • Convenciones de equipo e inversión existente. Una base de código, un pipeline de contratación, un sistema de diseño, y una CI construidos en torno a React son en sí mismos un activo. La postura de auditoría aquí es eliminar componentes específicos donde la plataforma ahora es suficiente — no migrar stacks completos.

La acción práctica: abre tu directorio de componentes React más grande, busca Modal, Popover, Tooltip, Dropdown, y cualquier importación de useResizeObserver. Cada uno es un candidato para el reemplazo nativo descrito anteriormente. Verifica el estado Baseline de la API en MDN para tu rango de navegadores soportados, distribuye el cambio detrás de un feature flag, y mide el delta del bundle. El navegador se ha puesto al día — el trabajo restante es auditar qué dependencias ya no necesitas.

Preguntas Frecuentes

¿Puedo usar el elemento dialog nativo con el modelo de estado de React?

Sí. Adjunta una ref al elemento dialog y llama a ref.current.showModal() o ref.current.close() desde efectos controlados por el estado de React. El dialog permanece en el árbol de React y acepta hijos JSX con normalidad, pero evitas las props de apertura controladas por useState en la salida renderizada. La principal fricción es que React no vuelve a ejecutar efectos para el evento cancel interno del dialog, así que adjunta un listener de cierre nativo mediante useEffect para sincronizar el estado de vuelta.

¿Cómo pasan los Custom Elements datos complejos a React, y viceversa?

React 19 pasa las props que no son cadenas de texto directamente a las propiedades del custom element en lugar de serializarlas como atributos, por lo que los objetos y arrays funcionan sin codificación JSON. Los custom elements devuelven datos mediante CustomEvent, que React 19 escucha usando props de manejador con el prefijo on (por ejemplo, onMyEvent). En React 18 y versiones anteriores, debes adjuntar los event listeners de forma imperativa mediante una ref, ya que los eventos sintéticos no gestionan nombres de eventos personalizados.

¿Las container queries y el selector :has() afectan al rendimiento de renderizado?

Ambos tienen un coste medible, pero generalmente son más económicos que las alternativas en JavaScript que reemplazan. Las container queries requieren que el navegador mantenga un contexto de contención y reevalúe las reglas coincidentes ante cambios de tamaño, lo cual sigue siendo más rápido que un callback de ResizeObserver que desencadena un render de React. El selector :has() puede ser costoso cuando se usa con selectores de sujeto amplios en árboles DOM de gran tamaño; limita su alcance a padres específicos en lugar de aplicarlo a elementos body o de nivel raíz.

¿Funciona la View Transitions API con routers del lado del cliente como React Router?

Sí, para transiciones dentro del mismo documento. Envuelve el callback de navegación de tu router en document.startViewTransition() para que la actualización del DOM que React realiza durante el cambio de ruta se ejecute dentro de la transición. React Router v6 y TanStack Router soportan este patrón mediante la interceptación de navegación. Las view transitions entre documentos, que animan cargas de página completas, requieren una activación adicional mediante la regla CSS @view-transition y tienen un soporte de navegadores más limitado — verifica en MDN antes de confiar en ellas.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.