Migrar un proyecto de JavaScript vainilla a React es uno de los ejercicios más reveladores cuando estás aprendiendo el ecosistema. El reto consiste en tomar Platzi Movies, la aplicación construida en los cursos de consumo de APIs con JavaScript, y replicarla usando ReactJS junto a React Router para manejar la navegación entre vistas.
Qué debes construir en este reto de Platzi Movies con React
La idea es replicar la misma experiencia que ya tenías con JavaScript puro, pero apoyándote en las herramientas que React pone sobre la mesa.
La aplicación original incluye varias vistas que dependen de rutas distintas, así que tu clon necesita reproducir cada una de ellas:
Página de tendencias con infinite scrolling para cargar más películas a medida que haces scroll.
Página de búsquedas que recibe un término y muestra los resultados desde la API.
Filtro por categorías para navegar entre géneros de películas.
Vista de detalle para cada película.
¿Qué es Platzi Movies? Es un proyecto práctico que consume la API de películas y muestra tendencias, búsquedas y categorías. Se construye primero en JavaScript vainilla y luego se migra a React como ejercicio de aprendizaje.
Por qué cambiar el hash router por React Router
En la versión original del proyecto, la navegación se resolvió a mano. El código usaba un hash router propio, hecho con lo mínimo necesario para funcionar: literalmente se llamaba a location.hash para decidir qué ruta renderizar en cada momento.
Ese enfoque funciona, pero te obliga a mantener tú mismo toda la lógica de rutas. Aquí es donde React Router cambia el juego, porque te entrega componentes y hooks listos para declarar rutas, leer parámetros y navegar entre vistas sin reinventar la rueda.
¿Qué es un hash router? Es un sistema de navegación que usa el símbolo # en la URL para diferenciar rutas sin recargar la página. Lee location.hash y renderiza la vista correspondiente según ese valor.
Qué ventajas te da React Router frente al router casero
Al migrar la navegación, vas a notar varias mejoras concretas en tu flujo de trabajo:
Declaras las rutas como componentes, no como condicionales sobre el hash.
Manejas parámetros dinámicos (como el id de una película) con hooks dedicados.
Puedes anidar rutas y compartir layouts entre vistas.
La navegación se siente fluida porque React solo re-renderiza lo necesario.
Con esto, lo que antes eran decenas de líneas de lógica manual se convierte en una estructura declarativa mucho más fácil de leer y mantener.
Cómo entregar tu clon de Platzi Movies en React
La entrega del reto es simple, pero importante para que recibas feedback real sobre tu código.
Compárteme el paso a paso en los comentarios y enlaza tu repositorio con el código del clon. Si quieres ir un paso más allá, despliega la aplicación en algún servicio de hosting y suma el enlace del sitio en vivo. Así no solo tienes el proyecto en JavaScript vainilla, también lo tienes con React funcionando para cualquiera que quiera revisarlo.
¿Ya empezaste tu clon? Cuéntame qué parte te está costando más: la migración de la navegación, el infinite scrolling o el filtro por categorías.
Ya lo tenía planeado :eyes: en un futuro voy a trabajar a full mi platzi movies con muchas más herramientas. TMDB tiene una gran API, muy recomendada para proyectos profesionales.
También tenía pensado recrear Platzi Movies con lo aprendido de React, ¡y sorpresa!, es un reto de este curso.
.
El archivo src/index.css contienes solo los estilos compartidos entre toda la aplicación. Se pueden ver más a detalle en el repositorio de GitHub correspondiente.
.
/* General */*{ box-sizing: border-box;}:root {--purple-light-1: #fbfafb;--purple-light-2: #eeeaf2;--purple-medium-1: #aa83c8;--purple-medium-2: #8b48bf;--purple-medium-3: #5c218a;--purple-dark-1: #3e0f64;--purple-dark-2: #2a0646;--yellow: #eecc75;--green: #cad297;--aqua: #b7eac5;--lightBlue: #a2eee5;--darkBlue: #8ea2ea;--red: #f09d9d;--font-family-titles:"Dosis", sans-serif;--font-family-text:"Red Hat Display", sans-serif;--font-weight-title1:800;--font-weight-title2:700;--font-weight-text1:400;--font-weight-text2:500;}html { background-color:var(--purple-medium-3);}// Otros estilos/* Shared */.header-container,.trendingPreview-header,.categoriesPreview-container,.liked-header {padding:0 24px;}// Más estilos/* Navigation */.inactive{display: none !important;}@keyframes loading-skeleton {0%,100%{opacity:100%;}50%{opacity:0%;}}
.
En el archivo App.js es donde implementamos nuestro HashRouter.
.
.
Cada uno de estas rutas contiene un archivo index.js que es el componente o página que vamos a renderizar dependiendo de la ruta a la que vamos a acceder.
.
Dentro de hook tenemos a nuestros custom hooks.
.
.
Dentro de componentes se encuentran los componentes generales que conforman las páginas o rutas en routes.
.
.
Cada uno de estos componentes contiene un archivo index.js y su correspondiente hoja de estilos.
.
A continuación mostraremos algunos de los archivos más importantes de la aplicación.
.
Omitimos la explicación de las hojas de estilos porque simplemente se movieron los estilos hacia los componentes pertinentes. Sin embargo, estas se pueden ver más a detalle en el repositorio de GitHub correspondiente.
.
.
El hook useTMDBApi facilita la obtención de datos de la API de The Movie Database (TMDB) desde un componente React. Acepta un endpoint y parámetros opcionales params, construye la url de la solicitud y administra el estado de la carga, los datos, el error y la página máxima maxPage para poder hacer un control sobre la paginación. Utiliza useEffect para realizar la solicitud cuando cambia la URL, actualizando los estados correspondientes según el resultado de la solicitud. Devuelve un objeto con los datos obtenidos, el estado de carga, el error y el número máximo de páginas disponibles en la respuesta.
.
El react hook useObserver.
.
.
El hook useObserver utiliza la API IntersectionObserver para observar si un elemento del DOM es visible en la ventana del navegador (viewport). Cuando el elemento se vuelve visible, ejecuta una función de callback. El hook devuelve una referencia elementRef que debe asignarse al elemento del DOM que se quiere observar. Además, limpia el observador cuando el componente se desmonte o cuando cambian las dependencias.
.
El react hook useLocalStorage.
.
.
El hook useLocalStorage gestiona el estado sincronizado con el almacenamiento local del navegador localStorage. Inicializa el estado con un valor almacenado en localStorage o un valor inicial proporcionado initialState. Proporciona funciones para obtener, establecer y agregar elementos al almacenamiento local, asegurando que las actualizaciones en localStorage se reflejen en el estado del componente. Además, establece el valor inicial en localStorage si no está presente. El hook devuelve el estado actual item y las funciones para interactuar con el almacenamiento local.
.
El hook useLikedMovies.
.
.
El hook useLikedMovies gestiona una lista de películas favoritas almacenadas en localStorage. Utiliza el hook useLocalStorage para inicializar, agregar y establecer películas en la lista de favoritos bajo la clave "liked_movies". Proporciona una función likeMovie que añade una película a la lista de favoritos si no está ya en ella, o la elimina si ya está presente. Este hook devuelve la lista de películas favoritas likedMovies y la función likeMovie para actualizar esta lista.
.
El hook useLazyLoading.
.
.
El hook useLazyLoading permite la carga diferida de imágenes en una lista de elementos items. Utiliza una referencia mutable imgRefs para almacenar referencias a las imágenes y la API IntersectionObserver para observar cuándo las imágenes se vuelven visibles en la ventana del navegador (viewport). Cuando una imagen se vuelve visible, se carga su URL desde un atributo "data-img", y si la URL termina en "null", se reemplaza con una imagen de error. Una vez cargada la imagen, se deja de observar. El hook devuelve la referencia de imágenes imgRefs que debe asignarse a cada imagen en el componente que utiliza este hook.
.
.
El componente HomePage combina varios componentes para crear la página de inicio de la aplicación. Utiliza el hook useTMDBApi para obtener datos de categorías y tendencias de películas desde la API de TMDB, y el hook useLikedMovies para gestionar las películas favoritas. Renderiza los componentes Header, TrendingPreview, CategoriesPreview, LikedMovies y Footer, pasando los datos obtenidos y las funciones necesarias como props. Muestra un listado de películas en tendencia, categorías de películas y las películas favoritas del usuario.
.
La página de CategoryPage.
.
.
El componente CategoryPage muestra películas basadas en una categoría específica seleccionada por el usuario. Utiliza hooks de React y hooks personalizados como useTMDBApi para obtener los datos de las películas por categoría y useLikedMovies para gestionar las películas favoritas. Los datos se actualizan y almacenan en el estado local utilizando useState y useEffect. Utiliza el componente GenericList para mostrar las películas y permite cargar más películas al desplazarse hacia abajo mediante una función de paginación.
.
La página de MovieDetailPage.
.
.
El componente MovieDetailPage muestra los detalles de una película seleccionada, así como las recomendaciones de películas relacionadas. Utiliza useParams para obtener el ID de la película de la URL, y hooks personalizados como useTMDBApi para obtener los datos de la película y las recomendaciones desde la API de TMDB. También utiliza useLikedMovies para gestionar las películas favoritas. Renderiza el componente Header con la imagen del póster de la película y el componente MovieDetail para mostrar los detalles y las películas relacionadas.
.
La página de NotFoundPage.
.
.
El componente SearchMoviePage permite a los usuarios buscar películas utilizando una barra de búsqueda. Utiliza useLocation y useSearchParams para obtener la consulta de búsqueda desde la URL o el estado de la navegación. Almacena los resultados de la búsqueda y la página actual en el estado local utilizando useState. Utiliza useTMDBApi para obtener datos de la API de TMDB y useLikedMovies para gestionar las películas favoritas. Cuando cambia la consulta de búsqueda, se restablecen los resultados y la página. Renderiza el componente Header y GenericList para mostrar los resultados de la búsqueda y permite la paginación para cargar más resultados.
.
La página de TrendsPage.
.
.
El componente TrendsPage presenta las películas en tendencia del día utilizando la API de TMDB. Utiliza el hook useTMDBApi para obtener datos de las películas en tendencia, manejando el estado de carga y la paginación. También emplea useLikedMovies para gestionar las películas favoritas. Se mantiene el estado local de la página actual y los resultados obtenidos, actualizándose cuando se reciben nuevos datos de la API. El método addNextPage incrementa la página actual para cargar más resultados. El componente renderiza un Header para la navegación y un GenericList para mostrar las películas en tendencia, permitiendo la interacción con la lista de favoritos y la carga de más contenido conforme el usuario navega.
.
.
El componente Movie representa una tarjeta de película individual con su imagen y un botón para marcarla como favorita. Cuando se hace clic en la tarjeta, se navega a una página de detalles de la película utilizando navigate. La imagen de la película se carga perezosamente utilizando una referencia imgRefs. El botón de favorito cambia su estilo si la película está en la lista de películas favoritas likedMoviesIds y permite agregar o eliminar la película de esa lista con la función likeMovie.
.
El componente MovieList.
.
.
El componente MovieList muestra una lista de películas. Utiliza el hook useLazyLoading para cargar las imágenes de las películas de manera diferida. Navega a los detalles de la película cuando se hace clic en una tarjeta de película y permite marcar o desmarcar películas como favoritas. Si no hay películas para mostrar, muestra un loading skeleton de la lista de películas.
.
El componente Category.
.
.
El componente Category muestra una lista de categorías de películas. Cada categoría tiene un título que, al hacer clic, navega a una página de lista de películas filtrada por esa categoría usando useNavigate. Si no hay categorías disponibles, muestra un loading skeleton.
.
El componente LikedMovies.
.
importReactfrom"react";import{MovieList}from"../MovieList";functionLikedMovies({ likeMovie, likedMovies }){return(<sectionid="liked"className="liked-container"><divclassName="liked-header"><h2className="liked-title">Favorite movies</h3></div><articleclassName="liked-movieList">{likedMovies ?(<MovieListmovies={likedMovies}likeMovie={likeMovie}likedMovies={likedMovies}/>):(Array(3).fill().map((_, index)=>(<divclass="movie-container"><imgkey={index}src="https://image.tmdb.org/t/p/w300/adOzdWS35KAo21r9R4BuFCkLer6.jpg"class="movie-img"alt="Nombre de la película"/></div>)))}</article></section> );
}
export { LikedMovies };
.
El componente LikedMovies muestra una lista de películas que han sido marcadas como favoritas. Utiliza el componente MovieList para renderizar las películas favoritas, pasando la función likeMovie y la lista de películas favoritas como props. Si no hay películas favoritas, muestra un estado de carga con imágenes de películas por defecto.
.
El componente Header.
.
.
El componente Header es un encabezado dinámico que cambia su contenido y estilo en función de la ruta actual. Utiliza hooks de React Router para gestionar la navegación y los parámetros de búsqueda. Muestra un título, una flecha para volver atrás y un formulario de búsqueda, cuyo estado se actualiza según la consulta de búsqueda en la URL. Si moviePoster está presente, establece una imagen de fondo en el encabezado. Este componente se adapta a diferentes páginas como detalles de películas, tendencias, categorías, búsqueda y la página de inicio.
.
El componente TrendingPreview.
.
.
El componente TrendingPreview muestra una vista previa de las películas en tendencia. Incluye un encabezado con un título y un botón que navega a una página de tendencias completa al hacer clic. Utiliza el componente MovieList para renderizar una lista de películas, pasando las películas en tendencia, la función para marcar como favorita y la lista de películas favoritas como props.
.
El componente CategoriesPreview.
.
.
El componente CategoriesPreview muestra una vista previa de las categorías de películas. Incluye un título y utiliza el componente Category para renderizar una lista de categorías. Cada categoría se muestra en un contenedor y permite la navegación a una página de lista de películas filtrada por esa categoría.
.
El componente Footer.
.
importReactfrom"react";import"./Footer.css";functionFooter(){return<footer>Powered by HaroldZS</footer>;}export{Footer};
.
El componente Observer utiliza el hook useObserver para detectar cuándo el div con la clase "observer" entra en el viewport. Este componente toma una función callback que se ejecuta cuando el elemento es visible en la pantalla. Utiliza una referencia observerRef que se pasa al div, activando así la observación y ejecución de la callback cuando el div se intersecta con el viewport.
.
El componente GenericList.
.
.
El componente GenericList muestra listas genéricas de películas usando el componente MovieList. Si está cargando, no muestra las películas; si no, renderiza las películas en páginas sucesivas. Además, utiliza el componente Observer para cargar más contenido cuando el usuario se desplaza al final de la lista. Si no está cargando y aún hay más páginas por cargar, el Observer activa la función addNextPage para obtener más películas.
.
El componente MovieDetail.
.
importReactfrom"react";import"./MovieDetail.css";import{MovieList}from"../MovieList";import{Category}from"../Category";functionMovieDetail({ movieData, relatedMovies, likeMovie, likedMovies, categories,}){return(<><sectionid="movieDetail"className="movieDetail-container">{movieData ?(<><h1className="movieDetail-title">{movieData.title}</h2><spanclassName="movieDetail-score">{movieData.vote_average}</span><pclassName="movieDetail-description">{movieData.overview}</p></> ) : (
<><h1className="movieDetail-title">Deadpool</h2><spanclassName="movieDetail-score">7.6</span><pclassName="movieDetail-description"> Wisecracking mercenary Deadpool battles the evil and powerful
Cable and other bad guys to save a boy's life.
</p></> )}
<articleclassName="categories-list"><Categorycategories={categories}/></article><articleclassName="relatedMovies-container"><h2className="relatedMovies-title">Related movies</h3><divclassName="relatedMovies-scrollContainer"><MovieListmovies={relatedMovies}likeMovie={likeMovie}likedMovies={likedMovies}/></div></article></section></> );
}
export { MovieDetail };
.
El componente MovieDetail muestra los detalles de una película seleccionada, incluyendo su título, puntuación y descripción. También muestra una lista de categorías relacionadas utilizando el componente Category y una lista de películas relacionadas con el componente MovieList. Si no hay datos de la película disponibles, muestra un ejemplo con información de "Deadpool".
.
Esta es la solución que implementé, aunque hay muchos aspectos que todavía puedo mejorar.
Hola a todos, les comparto mi clonación de PlatziMovies.
Despliegue:
Reto: PlatziMovies con React Router
El reto consiste en agarrar el proyecto de Platzi Movies el que construimos en los cursos de Consumo de API REST con Javascript en el práctico y en el profesional, para replicarlo en React JS.
.
Esta aplicación tiene navegación, infinite scrolling, lazy loading entre otras cosas, donde incluso teníamos nuestro propio router hecho a mano para simular un hash router que nos permita realizar navegación a partir de un hash.
.
Llamábamos a location.hash para saber cuando renderizar una ruta u otra. Entonces debes replicar ese mísmo comportamiento pero con React y React Router DOM.