Qué se debe optimizar en el frontend (y qué no)

1

Optimización de Proyectos con APIs REST en JavaScript

2

Optimización de Consumo de API REST en Frontend

3

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

4

Estrategias de Pantallas de Carga: Spinners vs Skeletons

5

Implementación de Pantallas de Carga con CSS y HTML

6

Implementación de Intersection Observer para Lazy Loading en Imágenes

7

Implementación de Lazy Loading con Intersection Observer

8

Manejo de Errores y Lazy Loading en Imágenes de Aplicaciones Web

Quiz: Optimización de imágenes

Paginación

9

Comparación: Paginación vs. Scroll Infinito en Aplicaciones Web

10

Implementación de Botón para Cargar Más con Paginación API

11

Scroll Infinito en Aplicaciones Web: Implementación y Mejores Prácticas

12

Implementación de Límite en Infinite Scrolling con APIs

13

Implementación de Closures en Paginación Infinita con JavaScript

Quiz: Paginación

Almacenamiento local

14

Almacenamiento Local con Local Storage en JavaScript

15

Maquetación y Estilos para Sección de Películas Favoritas

16

Uso de LocalStorage para Guardar y Recuperar Datos en JavaScript

17

Gestión de Películas Favoritas con Local Storage en JavaScript

Quiz: Almacenamiento local

Bonus

18

Internacionalización y Localización en Aplicaciones Web

19

Despliegue Seguro de Aplicaciones Web y Protección de API Keys

Próximos pasos

20

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

Implementación de Closures en Paginación Infinita con JavaScript

13/20
Recursos

¿Cómo implementar un infinite scroll usando closures?

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.

¿Qué son los closures y por qué son útiles aquí?

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.

¿Cómo aplicar closures para manejar paginaciones?

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.

¿Qué diferencias existen al implementar búsquedas y categorías con infinite scroll?

Adaptar el infinite scroll para diferentes rutas requiere ajustar algunos aspectos clave:

  1. 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
      }
    }
    
  2. 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
      }
    }
    

¿Cómo implementar estos closures en la navegación?

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.

Siguientes pasos: ¿Cómo mejorar con el almacenamiento local?

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

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

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:

  • document.onscroll = () => getPaginatedMoviesCategory(categoryId)()
  • document.onscroll = () => getPaginatedMoviesSearch(query)()

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.

Con estas notas logré entender lo que es \[Closure]\(https://github.com/aleroses/Platzi/blob/master/DW/2-intermedio/006.closures\_scope-en-js/closures\_scope-en-js.md#ejemplo-de-closure) espero sirva...
Para las funciones con paginación que tienen parámetro, yo volví a obtener el parámetro dentro del código de la función "Paginated". Ejemplo con las películas por categoría "const \[\_,category] = location.hash.split('=');    const \[idcat,categName]  = category.split('-');" luego pasé el idcat como parámetro en la llamada a la api. No se si es lo mejor, pero funciona.

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)
    }
  }
}