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.
if(scrollIsBottom && pageIsNotMax){ page++;const{ data }=awaitapi('/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.
En la navegación, cada vez que se añade el evento del scroll con la funcion de paginación a ejecutar, se debería de devolver la page = 1, para limpiar la variable de las posibles navegaciones que se realicen en otras secciones, con esto aseguramos que el endpoint que se vaya a ejecutar cargue de la página 1++.
Hola, tengo un par de apuntes que yo considero importantes para compartir:
Los closures son una característica poderosa de JavaScript que permite crear funciones que pueden recordar el estado de las variables en el momento en que se crearon, lo que permite una mayor flexibilidad y funcionalidad en el código.
Por otro lado, el currying es una técnica de programación funcional que consiste en transformar una función con varios argumentos en una serie de funciones que toman uno o más argumentos. La función curried devuelve otra función que toma el siguiente argumento, y así sucesivamente, hasta que se han proporcionado todos los argumentos necesarios.
En ambas técnicas, se utilizan funciones anidadas en JavaScript para lograr su funcionalidad. En el caso de los closures, la función interna (anidada) se utiliza para acceder a las variables de su ámbito externo, mientras que en el currying, la función interna (anidada) se utiliza para devolver una nueva función que toma un argumento adicional.
Falto resetear la variable page
Puede que faltara asignar a la variable global "page" el valor 1, debido al pasar a otra sección(vista de películas por categorías, vista de películas por busqueda) si el usuario ha navegado previamente en otras secciones que modifican a page como la de trending movies probablemente el valor de page tendrá un valor mayor a 1 y ese valor será utilizado para solicitar películas en las demás secciones que lo requieran al realizar un scroll.
En mi caso habia inhabilitado a 'infititeScroll', asi que use el doble paréntesis que dijo el profesor si no se trabajara con el addEventListener, para invocar a la función de paginación:
Hmmm... Baja un poco la reusabilidad de esa función. Pero para efectos prácticos de que solo la usaremos en este proyecto tiene todo el sentido del mundo. :clap:
Continuando con mi aporte de la clase pasada, y mejorándolo gracias a lo aprendido en esta clase, ya no es necesario volver a conseguir el query ni el categoryId del hash como lo hacía:
Quería comentar, que agregué un patrón Throttle para evitar la repetición innecesaria de peticiones, durante el scrolling. Pueden consultar más del tema aquí. Thottle y Debounce nos ayudan a gestionar mejor la ejecución de callbacks, y peticiones, en situaciones propensas a cuellos de botella. Adjunto el repositorio de mi código.
El infinite scrolling de la vista de películas de categorías me quedó así:
y sleep, una función auxiliar para aplicar delay al código, que en este caso se usa para darle margen a la petición (promesa) de completarse, sin que concurrentemente, se inicien nuevas peticiones al mismo URL, en este caso páginación. (Misma página / datos redundantes).
Hola, si tienen el error de que no les funciona la funcion de, porque como copiamos la misma funcion de getMoviesBySearch y esta viene con un async, debemos de quitarselo, aqui un ejemplo mas claro