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)
Tu responsabilidad como frontend developer
Caché vs. memoria
Debuggeando caché y networking
Quiz: Qué se debe optimizar en el frontend (y qué no)
Optimización de imágenes
Loading spinners vs. loading skeletons
Reto: pantalla de carga
Intersection Observer
Lazy Loading
Imágenes por defecto
Quiz: Optimización de imágenes
Paginación
Scroll infinito vs. paginación
Botón de cargar más
Infinite Scrolling: evento de scroll
Infinite Scrolling: limitando la carga de datos
Infinite Scrolling: closures de navegación
Quiz: Paginación
Almacenamiento local
Local Storage vs. API real
Botón de like
Guardando películas en Local Storage
Lista de películas favoritas
Quiz: Almacenamiento local
Bonus
Reto: selección de idioma
Deploy
Próximos pasos
Más proyectos para tu portafolio
No tienes acceso a esta clase
¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera
Juan David Castro Gallego
Aportes 26
Preguntas 5
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++.
https://www.youtube.com/watch?v=JXG_gQ0OF74
A pesar de que aún no los domino, este videíto del Profesor Sasha en su canal de La Cocina del Código (canal super recomendado 😎✨) me ayudó mucho a entender cómo funcionan los closures con profundidad, je je.
Yo como no tengo seccion de tendencias hacia abajo lo implemente en search primero y lo habia puesto:
infiniteScroll = ()=> getPaginatedMovies(keyword) ;
Es la primera vez que veo útil los closures en js.
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');
};
}
FUN FACT: “Drama” es la categoría con mas películas.
Tiene +9,400 pages x 20movies = 188,000movies
Yo lo resolvi de la siguiente forma:
infiniteScroll = () => { getPaginatedCategories(categoryId)};
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:
yo implemente esta solucion y me funciono bien hasta donde he probado
infiniteScroll = () => {
getPaginatedMoviesBySearch(realQuery)}
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.
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.
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?