Closures para infinite scroll en múltiples rutas

Resumen

Implementar infinite scroll en múltiples rutas de una aplicación parece sencillo hasta que necesitas pasar parámetros distintos a cada función de paginación. Aquí es donde los closures en JavaScript se convierten en la herramienta más elegante para resolver el problema, permitiéndote reutilizar la misma lógica de scroll para tendencias, búsquedas y categorías sin duplicar código.

¿Por qué usar closures para el infinite scroll?

Cuando ya tienes funcionando getPaginatedTrendingMovies con su validación de scroll y su límite de páginas, replicar ese comportamiento en búsquedas y categorías exige enviar parámetros adicionales como query o el ID de la categoría. El problema aparece al asignar la función al evento de scroll: necesitas pasarle el nombre de la función, no su ejecución.

¿Qué es un closure en JavaScript? Es una función que recuerda las variables del ámbito donde fue creada, incluso después de que ese ámbito haya terminado de ejecutarse. Permite "encerrar" parámetros para usarlos más tarde.

Si escribes infiniteScroll = getPaginatedMoviesBySearch(query), estás ejecutando la función inmediatamente en lugar de guardar su referencia. El resultado: el scroll deja de disparar nuevas peticiones cuando llegas al final de la página [05:30].

¿Cómo se construye un closure para paginación?

La idea consiste en transformar tu función de paginación en una función que devuelve otra función. La función externa recibe el parámetro que necesitas conservar (query o categoryId), y la función interna ejecuta toda la lógica de scroll, validación y consulta a la API.

javascript function getPaginatedMoviesBySearch(query) { return async function () { const { scrollTop, scrollHeight, clientHeight } = document.documentElement; const scrollIsBottom = (scrollTop + clientHeight) >= (scrollHeight - 15); const pageIsNotMax = page < maxPage;

if (scrollIsBottom && pageIsNotMax) { page++; const { data } = await api('/search/movie', { params: { query, page }, }); const movies = data.results; createMovies(movies, genericSection, { lazyLoad: true, clean: false }); }

}; }

Al asignarla con infiniteScroll = getPaginatedMoviesBySearch(query), ejecutas únicamente la función externa, que captura query en su closure y te devuelve la función interna lista para dispararse en cada evento de scroll.

¿Qué hace exactamente la función externa?

Su único trabajo es recibir el parámetro y devolver la función real de paginación. No hace peticiones, no valida scroll, no renderiza. Es un envoltorio cuya razón de existir es preservar el valor que la función interna usará después.

¿Y la función interna?

Es la que se ejecuta en cada scroll. Gracias al closure, tiene acceso a query aunque la función externa ya haya terminado. Aquí ocurre la validación del scroll, el incremento de página, la consulta al endpoint /search/movie y la renderización con lazyLoad: true y clean: false para no borrar los resultados anteriores.

¿Cómo aplicar el mismo patrón a categorías?

La lógica se replica casi idéntica para getPaginatedMoviesByCategory, pero con dos cambios clave: el endpoint pasa a ser /discover/movie y el parámetro enviado es withGenres con el ID de la categoría [12:45].

  • Recibes categoryId en la función externa.
  • Devuelves una función asíncrona que ejecuta la consulta.
  • Defines maxPage = data.totalPages después de la primera petición.
  • Mantienes page++ para que la paginación avance.
  • Renderizas con createMovies(movies, genericSection, { lazyLoad: true, clean: false }).

¿Por qué usar clean: false en cargas posteriores? Porque no queremos limpiar los filmes ya renderizados. La primera carga puede ir con clean: true, pero las siguientes deben acumular resultados para que el scroll infinito tenga sentido.

En la navegación, cuando el usuario entra a una categoría, asignas infiniteScroll = getPaginatedMoviesByCategory(categoryId). La ejecución captura el ID, y el evento de scroll dispara la función interna cuantas veces sea necesario.

¿Qué ganas al combinar closures con infinite scroll?

Esta combinación te permite mantener una única variable global infiniteScroll que cambia su comportamiento según la ruta activa. La API de tendencias devuelve hasta 500 páginas en algunos casos [10:15], y con esta arquitectura puedes recorrerlas todas sin reescribir lógica.

  • Reutilizas la misma estructura para tendencias, búsquedas y categorías.
  • Evitas ejecutar la función al asignarla al evento de scroll.
  • Conservas parámetros dinámicos como query o categoryId sin variables globales.
  • Mantienes el control del límite de páginas con maxPage = data.totalPages.

El patrón funciona porque JavaScript trata las funciones como ciudadanos de primera clase: puedes devolverlas, pasarlas como argumentos y guardarlas en variables. Y aquí viene lo interesante: cada vez que llamas a la función externa con un parámetro distinto, obtienes una nueva función interna con su propio closure, totalmente independiente.

Si todavía sientes que el concepto de closure se te escapa, vuelve a la documentación oficial de closures en JavaScript y considera el curso específico sobre scope y closures antes de seguir avanzando. Cuéntame en los comentarios en qué otra parte de tu proyecto aplicarías este patrón.