Deben quitar el async de la función “invocadora” del closure, si lo dejan, cosas raras pasan y se pueden pasar dos horas debuggeando.
Odio y adoro este tipo de clases.
Qué se debe optimizar en el frontend (y qué no)
Optimización de Proyectos con APIs REST en JavaScript
Optimización de Consumo de API REST en Frontend
Uso del Inspector de Elementos y Network en Navegadores
Quiz: Qué se debe optimizar en el frontend (y qué no)
Optimización de imágenes
Estrategias de Pantallas de Carga: Spinners vs Skeletons
Implementación de Pantallas de Carga con CSS y HTML
Implementación de Intersection Observer para Lazy Loading en Imágenes
Implementación de Lazy Loading con Intersection Observer
Manejo de Errores y Lazy Loading en Imágenes de Aplicaciones Web
Quiz: Optimización de imágenes
Paginación
Comparación: Paginación vs. Scroll Infinito en Aplicaciones Web
Implementación de Botón para Cargar Más con Paginación API
Scroll Infinito en Aplicaciones Web: Implementación y Mejores Prácticas
Implementación de Límite en Infinite Scrolling con APIs
Implementación de Closures en Paginación Infinita con JavaScript
Quiz: Paginación
Almacenamiento local
Almacenamiento Local con Local Storage en JavaScript
Maquetación y Estilos para Sección de Películas Favoritas
Uso de LocalStorage para Guardar y Recuperar Datos en JavaScript
Gestión de Películas Favoritas con Local Storage en JavaScript
Quiz: Almacenamiento local
Bonus
Internacionalización y Localización en Aplicaciones Web
Despliegue Seguro de Aplicaciones Web y Protección de API Keys
Próximos pasos
Optimización de Aplicaciones Frontend con APIs REST y JavaScript
No tienes acceso a esta clase
¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera
El infinite scroll es una característica moderna en el desarrollo de aplicaciones web que permite cargar contenido de manera continua. En esta guía, exploraremos cómo hacerlo efectivo usando closures. Este enfoque no solo es práctico, sino también elegante y sofisticado. Si bien los closures pueden parecer complejos, presentan una solución clara y eficiente para manejar paginaciones y búsquedas. A lo largo de este contenido, mostraremos cómo adaptarlos según sea necesario, delegando los parámetros correctos y optimizando el flujo de las funciones en JavaScript.
Un closure es una función que guarda referencias al estado o entorno externo (variables) en el que fue creado, incluso después de que dicha función ha finalizado su ejecución. En nuestro contexto, los closures nos permiten capturar un parámetro (como una keyword de búsqueda) sin ejecutarlo de inmediato, lo que resulta esencial para el correcto funcionamiento del infinite scroll. Este enfoque permite separar la preparación de los datos de su ejecución, lo que permite optimizar el rendimiento y la legibilidad del código.
Para implementar un infinite scroll utilizando closures, primero debemos entender cómo separar la lógica de obtención de datos y la implementación del scroll infinito en sí. Supongamos que queremos trabajar con dos tipos diferentes de funciones: búsquedas y categorías. Vamos a crear funciones que generan otras funciones y se mantendrán a la espera hasta que los datos correctos sean enviados.
function getPaginatedMoviesBySearch(query) {
return function() {
// Lógica de paginación usando 'query'
// Validamos y realizamos la consulta a la API
}
}
En este ejemplo básico, getPaginatedMoviesBySearch
es un closure que espera un query
. Cuando el scroll infinito debe generar más datos, ejecuta la función interna que lleva a cabo la lógica de carga y paginación.
Adaptar el infinite scroll para diferentes rutas requiere ajustar algunos aspectos clave:
Búsquedas: Necesitamos capturar un query
que se utilizará para buscar películas. La función debe fijarse si más páginas deben cargarse y gestionarlo adecuadamente.
function getPaginatedMoviesBySearch(query) {
return function() {
// Actualizamos la lógica para manejar la búsqueda junto al scroll
}
}
Categorías: Aquí, en lugar de un query
, necesitamos un categoryId
para filtrar las películas. Aprovechamos el mismo patrón modificando el endpoint.
function getPaginatedMoviesByCategory(categoryId) {
return function() {
// Lógica similar, pero personalizando para categorías
}
}
Al implementar los closures en nuestro manejador de navegación, el objetivo es delegar las funciones correctas cuando el usuario interactúa con la aplicación. Estas funciones deben apuntar correctamente a queries
o categoryIds
dependiendo de la ruta en la que se encuentren.
let infiniteScroll;
function onSearchPage(query) {
infiniteScroll = getPaginatedMoviesBySearch(query);
}
function onCategoryPage(categoryId) {
infiniteScroll = getPaginatedMoviesByCategory(categoryId);
}
Con este enfoque, al cambiar de página, simplemente llamamos a la función correcta y pasamos el parámetro necesario, sin preocuparnos por cómo interactúa el closure hasta que sea absolutamente necesario.
Una vez que comprendemos cómo implementar infinite scroll y manejar diferentes tipos de datos, el siguiente reto es examinar cuándo usar APIs reales frente al almacenamiento local. Esto ayudará a mejorar el rendimiento y a optimizar recursos. Recuerda, el local storage no debe usarse para almacenar datos privados o críticos debido a preocupaciones de seguridad. Sin embargo, es perfecto para almacenar datos que no cambian frecuentemente y que no son sensibles.
Te animamos a seguir explorando más sobre closures, prácticas de almacenamiento y a continuar perfeccionando tus habilidades como desarrollador. El mundo del desarrollo web es dinámico y estimulante, y cada pequeño concepto aprendido es un paso hacia la maestría. ¡Sigue adelante!
Aportes 25
Preguntas 6
Deben quitar el async de la función “invocadora” del closure, si lo dejan, cosas raras pasan y se pueden pasar dos horas debuggeando.
Odio y adoro este tipo de clases.
Resumen de Closure
.
Un closure es la combinación entre una función y el ámbito léxico en el que esta fue declarada. Con esto, la función recuerda el ámbito en el que se creó
Recuerda la asignación anterior
.
En esta clase se explica esto y se muestra un ejemplo
Ahora hay que implementar un botón para volver al inicio de la página cuando estemos haciendo el scroll infinito
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++.
Yo como no tengo seccion de tendencias hacia abajo lo implemente en search primero y lo habia puesto:
infiniteScroll = ()=> getPaginatedMovies(keyword) ;
Yo lo resolvi de la siguiente forma:
infiniteScroll = () => { getPaginatedCategories(categoryId)};
Excelente clase! Que gran uso de closures!
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:
main.js
function getPaginatedMoviesByCategory() {
const [_, categoryData] = location.hash.split('=');
const [categoryId] = categoryData.split('-');
getPaginatedMovies('/discover/movie', {categoryId});
}
function getPaginatedMoviesBySearch() {
const [_, undecodedQuery] = location.hash.split('=');
const query = decodeURI(undecodedQuery);
getPaginatedMovies('/search/movie', {undefined, query});
}
Sino que directamente se los paso como parámetro en navigation.js:
infiniteScroll = getPaginatedMoviesByCategory(categoryId);
infiniteScroll = getPaginatedMoviesBySearch(query);
Y retorno una función que ejecuta la función global de paginación:
main.js
function getPaginatedMoviesByCategory(categoryId) {
return function () {
getPaginatedMovies('/discover/movie', {categoryId});
}
}
function getPaginatedMoviesBySearch(query) {
return function () {
getPaginatedMovies('/search/movie', {undefined, query});
}
}
async function getPaginatedMovies(
endPoint,
{
categoryId,
query
} = {},
) {
const {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
const scrollAtBottom = (scrollTop + clientHeight) >= (scrollHeight - 15);
const pageIsNotMax = page < maxPage;
if (scrollAtBottom && pageIsNotMax) {
page++;
const { data } = await api(endPoint, {
params: {
page,
with_genres: categoryId,
query,
},
});
const movies = data.results;
printMoviePosters(movies, genericSection, true);
} else if (scrollAtBottom && !pageIsNotMax) {
maxPageReached.classList.remove('inactive');
};
}
Es la primera vez que veo útil los closures en js.
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.
yo implemente esta solucion y me funciono bien hasta donde he probado
infiniteScroll = () => {
getPaginatedMoviesBySearch(realQuery)}
FUN FACT: “Drama” es la categoría con mas películas.
Tiene +9,400 pages x 20movies = 188,000movies
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í:
const throttleGetMoviesByCategory = throttle(getMoviesByCategory, 250);
const scrollingMoviesByCategory = async () => {
const { page } = categoriesHistory;
if(scrollBottomReached()){
await throttleGetMoviesByCategory({...categoriesHistory, page: page + 1 })
}
}
En categoriesHistory solo alamcené las variables de navegación y estado de la vista de películas por categoría:
categoriesHistory = {
categoryId,
categoryName,
page,
total_pages,
};
La función throttle es:
const throttle = (cb, delay = 500) => {
let waiting = false;
return async (...args) => {
if (waiting) return;
waiting = true;
try {
await cb(...args);
}catch(err){
console.log("Error: ", err);
}
await sleep(delay);
waiting = false;
}
}
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).
const sleep = async (ms = 1000) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
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:
Les comparto mi solución para el scroll infinito para cada una de las páginas. 😄
Repositorio de GitHub
export const getPaginatedMovies = async () => {
const {
scrollTop: SCROLL_TOP,
scrollHeight: SCROLL_HEIGHT,
clientHeight: CLIENT_HEIGHT
} = document.documentElement;
const IS_SCROLL_BOTTOM = (SCROLL_TOP + CLIENT_HEIGHT) >= (SCROLL_HEIGHT - 15);
const IS_NOT_HOME = location.hash !== '#home';
if (IS_SCROLL_BOTTOM && IS_NOT_HOME) {
const HASHES_ROUTES = {
'#trends' : 'trending/movie/day',
'#category' : 'discover/movie',
'#search' : 'search/movie'
};
const DATA_OF_HASH = location.hash.split('=');
const HASH_NAME = DATA_OF_HASH[0];
const EXTRA_INFO = DATA_OF_HASH[1];
const ROUT = HASHES_ROUTES[HASH_NAME];
const params = {
page : pageMovies(),
with_genres : '',
query : ''
};
if (HASH_NAME === '#category') {
const [ID_CATEGORY] = EXTRA_INFO.split('-');
params.with_genres = ID_CATEGORY;
}
if (HASH_NAME === '#search') {
const QUERY_SEARCH = EXTRA_INFO;
params.query = QUERY_SEARCH;
}
const RESPONSE = await api(ROUT, { params });
const DATA = RESPONSE.data;
const MOVIES = DATA.results;
const IS_MAX_PAGE = DATA.page > DATA.total_pages;
if (IS_MAX_PAGE)
return;
const IS_CAROUSEL = false;
insertMovies(MOVIES, GENERIC_LIST_CONTAINER, IS_CAROUSEL, { clean: false });
}
};
export const currentPageMoviesUpdate = () => {
let pageMovies = 1;
return function (refresh = false) {
pageMovies = (refresh)
? 1
: pageMovies + 1;
return pageMovies;
};
};
Para los que no son fan de los closures les comparto mi solución al problema “como obtener el query”:
Espero les sea útil!
Este había sido como lo resolví
// Navigation.js
getPaginatedMoviesBySearch.query = query
getPaginatedMoviesBySearch = getPaginatedMoviesBySearch.bind(getPaginatedMoviesBySearch)
infiniteScroll = getPaginatedMoviesBySearch
// Function getPaginatedMoviesBySearch
query = this.query
Yo cuando hizo return de una función: *Quedé
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
ANTES NO FUNCIONANDO
const getPaginatedMoviesBySearch = async (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 });
}
};
};
FUNCIONANDO
La unica diferencia es la palabra reservada async que causa que no funcione.
const 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 });
}
};
};
Agregue el Infinite Scrolling a las películas relacionadas
function getPaginatedRelatedMovies(id) {
return async function() {
const scrollWidthScreen = document.documentElement.scrollWidth;
const { scrollLeft, scrollWidth } = relatedMoviesContainer;
const scrollIsEnd = ((scrollWidth + scrollLeft) >= (scrollWidthScreen - 100));
const pageIsNotMax = page < maxPage;
if (scrollIsEnd && pageIsNotMax) {
page++;
const { data } = await api(`movie/${id}/similar`, {
params: {
page
}
});
const movie = data.results;
printMovies(movie, relatedMoviesContainer, {clean: false, lazyLoad: true})
}
}
}
Agregué el evento listener al contenedor de películas relacionadas
relatedMoviesContainer.addEventListener("scroll", infiniteScroll, false);
El contenedor me empezó a dar problemas así que le agrege el max-height
.relatedMovies-scrollContainer {
position: absolute;
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
width: calc(100vw - 24px);
padding-bottom: 16px;
max-height: 187px;
}
Esta solucion me gusta bastante, yo en mi caso hice que infinite scroll sea una funcion anonima, en donde al ejecutarla por el evento scroll llame a la funcion paginada:
function searchPage(){
console.log('Search !!');
headerSection.classList.remove('header-container--long');
headerSection.style.background = '';
arrowBtn.classList.remove('inactive');
arrowBtn.classList.remove('header-arrow--white');
headerTitle.classList.add('inactive');
headerCategoryTitle.classList.add('inactive');
searchForm.classList.remove('inactive');
trendingPreviewSection.classList.add('inactive');
categoriesPreviewSection.classList.add('inactive');
genericSection.classList.remove('inactive');
movieDetailSection.classList.add('inactive');
const [ _ , query] = location.hash.split('=');
getMoviesBySearch(query);
infiniteScroll = () => {
getPaginetedMoviesBySearch(query)
};
}
Aqui en este caso tambien se aplicaria closure, ya que la funcion interna siempre recuerda esa variable de la funcion padre (query). Asi sea que la funcion padre (searchPage) haya terminado.
Tenía entendido esto de CLOSURE
Me estaba confundiendo un poco por la sintaxis del prof pero viéndolo bien si es el mismo concepto, ya que usa una variable del scope padre.
He querido aplicar DRY en el codigo, evitando la repeticion de la repetida al momento de obtener las peliculas por paginacion, lo he resuelto, aplicando objeto como parametro, el primero es la url o endpoint que solicitamos, el tercero, la query que deseamos consultar, bien sea el with_genres o el query comun y por defecto, para los trending le paso un null y por ultimo, hacia donde procede la solicitud, identificando esto, puedo adjuntarle a un objeto parameter que se le pasara a params en un llamado de axios, y alli, le agrego o el query o el with_genres, pense hacerlo con un Map pero como es un objeto que se crea en cada llamada, no lo vi tan necesario, espero tenga un buen perfomance y puedan ayudarme a mejorarlo
function getPaginatedMovies({url, query = undefined, searchBy = undefined}) {
return async function () {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement
const scrollIsBottom = scrollTop + clientHeight >= scrollHeight - 15
const pageIsNotMax = page < maxPage
const parameter = {
page
}
if (searchBy == 'category') parameter.with_genres = query
if (searchBy === 'search') parameter.query = query
if (scrollIsBottom && pageIsNotMax) {
page++
const { data } = await axios_api(url, {
params: parameter
})
const movies = data.results
const options = {
lazyload: true,
cleanScreen: false,
}
createPreviewsMovies(movies, genericSection, options)
}
}
}
¿Quieres ver más aportes, preguntas y respuestas de la comunidad?