Back

Construcción de Componentes Conscientes del Scroll en React

Construcción de Componentes Conscientes del Scroll en React

Tu manejador de scroll se dispara 60 veces por segundo. Tu componente se re-renderiza en cada tick. Tus usuarios ven saltos en lugar de animaciones suaves. Este patrón rompe las aplicaciones React más rápido que casi cualquier otro error de rendimiento.

Construir componentes conscientes del scroll en React requiere entender cuándo evitar completamente el sistema de estado de React. Este artículo cubre las herramientas adecuadas para el seguimiento de la posición de scroll en React, por qué Intersection Observer en React debería ser tu opción predeterminada, y cómo implementar un manejo de scroll eficiente cuando realmente necesitas datos de scroll en bruto.

Puntos Clave

  • Intersection Observer maneja la mayoría de los patrones conscientes del scroll sin costo de rendimiento
  • Cuando necesites valores continuos de scroll, usa refs y requestAnimationFrame en lugar de estado
  • Reserva useSyncExternalStore para estado de scroll compartido entre componentes
  • Prueba con el enrutamiento del framework y siempre proporciona alternativas de movimiento reducido

Por Qué Intersection Observer Debería Ser Tu Primera Opción

La mayoría de los patrones de UI impulsados por scroll en React no necesitan realmente la posición del scroll. Necesitan detección de visibilidad: ¿Está este elemento en pantalla? ¿Ha pasado el usuario de esta sección?

Intersection Observer maneja estos casos sin ejecutar código de aplicación en cada evento de scroll. El navegador optimiza la observación internamente, haciéndolo dramáticamente más eficiente que los listeners de scroll.

import { useInView } from 'react-intersection-observer';

function FadeInSection({ children }) {
  const { ref, inView } = useInView({
    threshold: 0.1,
    triggerOnce: true,
  });

  return (
    <div ref={ref} className={inView ? 'visible' : 'hidden'}>
      {children}
    </div>
  );
}

La librería react-intersection-observer envuelve esta API de forma limpia. Úsala para seguimiento de secciones, carga diferida y animaciones de revelación.

Cuándo Realmente Necesitas la Posición del Scroll

Algunos patrones requieren valores continuos de scroll: efectos parallax, indicadores de progreso o transformaciones vinculadas al scroll. Aquí es donde la mayoría de los tutoriales te fallan.

Nunca hagas esto:

// Esto causa re-renderizados en cada tick de scroll
const [scrollY, setScrollY] = useState(0);

useEffect(() => {
  const handleScroll = () => setScrollY(window.scrollY);
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

Este patrón desencadena el proceso de reconciliación de React más de 60 veces por segundo durante el scroll activo.

El Patrón Basado en Refs

Almacena los valores de scroll en refs y actualiza el DOM directamente:

import { useRef, useEffect } from 'react';

function ParallaxElement() {
  const elementRef = useRef(null);
  const rafId = useRef(null);

  useEffect(() => {
    const handleScroll = () => {
      if (rafId.current) return;
      
      rafId.current = requestAnimationFrame(() => {
        if (elementRef.current) {
          const offset = window.scrollY * 0.5;
          elementRef.current.style.transform = `translateY(${offset}px)`;
        }
        rafId.current = null;
      });
    };

    window.addEventListener('scroll', handleScroll, { passive: true });
    return () => {
      window.removeEventListener('scroll', handleScroll);
      if (rafId.current) cancelAnimationFrame(rafId.current);
    };
  }, []);

  return <div ref={elementRef}>Contenido parallax</div>;
}

Detalles clave: requestAnimationFrame limita las actualizaciones a la tasa de refresco de la pantalla. La opción { passive: true } le indica al navegador que no llamarás a preventDefault(), habilitando optimizaciones de scroll.

useSyncExternalStore para Estado de Scroll Compartido

Cuando múltiples componentes necesitan la posición del scroll, trátalo como un store externo:

import { useSyncExternalStore } from 'react';

const scrollStore = {
  subscribe: (callback) => {
    window.addEventListener('scroll', callback, { passive: true });
    return () => window.removeEventListener('scroll', callback);
  },
  getSnapshot: () => window.scrollY,
  getServerSnapshot: () => 0,
};

function useScrollPosition() {
  return useSyncExternalStore(
    scrollStore.subscribe,
    scrollStore.getSnapshot,
    scrollStore.getServerSnapshot
  );
}

Este patrón funciona correctamente con las características concurrentes de React y maneja el renderizado del lado del servidor. En la práctica, aún deberías limitar o reducir la granularidad de las actualizaciones (por ejemplo, emitiendo cambios de sección en lugar de píxeles en bruto) para evitar re-renderizados en cada frame de scroll.

useLayoutEffect vs useEffect para Scroll

Usa useLayoutEffect cuando necesites medir elementos del DOM antes de que el navegador pinte. Esto previene parpadeos visuales al calcular posiciones con getBoundingClientRect().

Usa useEffect para adjuntar listeners de scroll. La configuración del listener no necesita bloquear el pintado.

Consideraciones sobre Frameworks

Next.js App Router y frameworks similares gestionan la restauración del scroll automáticamente. Los manejadores de scroll personalizados pueden entrar en conflicto con este comportamiento. Prueba los componentes conscientes del scroll después de la navegación del lado del cliente para detectar problemas de restauración.

Animaciones CSS Impulsadas por Scroll

Los navegadores modernos soportan animation-timeline: scroll() para efectos de scroll puramente CSS. El soporte del navegador sigue siendo incompleto, pero este enfoque elimina JavaScript por completo para los casos soportados. Úsalo con mejora progresiva—proporciona una experiencia de respaldo para navegadores no soportados.

Accesibilidad

Siempre respeta prefers-reduced-motion. Envuelve las animaciones activadas por scroll:

const prefersReducedMotion =
  typeof window !== 'undefined' &&
  window.matchMedia('(prefers-reduced-motion: reduce)').matches;

Omite las animaciones o proporciona transiciones instantáneas cuando esto devuelva true.

Conclusión

Los componentes conscientes del scroll no tienen que hundir el rendimiento de tu aplicación React. La clave es elegir la herramienta adecuada para cada caso de uso. Recurre primero a Intersection Observer—maneja la detección de visibilidad sin sobrecarga de rendimiento. Cuando realmente necesites valores continuos de scroll, evita el sistema de estado de React usando refs y requestAnimationFrame para manipulación directa del DOM. Para estado de scroll compartido entre múltiples componentes, useSyncExternalStore proporciona un patrón limpio y seguro para concurrencia. Cualquiera que sea el enfoque que elijas, prueba exhaustivamente con el enrutamiento de tu framework y siempre respeta las preferencias del usuario para movimiento reducido.

Preguntas Frecuentes

Establecer el estado en cada evento de scroll desencadena el proceso de reconciliación de React más de 60 veces por segundo. Cada actualización de estado causa un re-renderizado, lo que significa que React debe comparar el DOM virtual y potencialmente actualizar el DOM real repetidamente. Esto sobrecarga el hilo principal y causa saltos visibles. Usar refs evita esto por completo ya que las actualizaciones de ref no desencadenan re-renderizados.

Usa useSyncExternalStore cuando múltiples componentes necesiten el mismo valor de posición de scroll. Se integra correctamente con las características de renderizado concurrente de React y maneja casos extremos como el tearing, donde diferentes componentes podrían ver valores diferentes durante un renderizado. También proporciona soporte integrado para renderizado del lado del servidor a través de getServerSnapshot.

Puedes usarlas con mejora progresiva. Las animaciones CSS impulsadas por scroll actualmente funcionan en Chrome y Edge, con soporte en Safari y Firefox aún incompleto o en progreso. Impleméntalas como una mejora para navegadores soportados mientras proporcionas un respaldo en JavaScript o una experiencia simplificada para otros. Consulta caniuse.com para el soporte actual del navegador antes de depender de ellas.

Prueba después de la navegación del lado del cliente, no solo en la carga inicial de la página. El App Router gestiona la restauración del scroll automáticamente, lo que puede entrar en conflicto con manejadores de scroll personalizados. Navega entre páginas usando componentes Link y verifica que tus listeners de scroll se vuelvan a adjuntar correctamente y no interfieran con el comportamiento de restauración de posición de scroll del framework.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay