38. 4 Routers
Tutorial 38.4: Implementando Router en nuestra App de Películas
Introducción a React Router
Ahora que tenemos nuestra aplicación mostrando películas, necesitamos implementar navegación entre diferentes vistas. Vamos a crear rutas para:
Ver la lista de películas (página principal)
Ver el detalle de cada película
Otras vistas futuras (búsqueda, favoritos, etc.)
Paso 1: Estructura de carpetas
Primero, vamos a organizar nuestro proyecto con una estructura más profesional:
text
src/
├── assets/
├── components/
│ ├── ContextMovieCard.jsx
│ └── MovieCard.jsx
├── data/
├── pages/
│ └── LandingPage.jsx
├── routers/
│ └── routes.jsx
├── App.css
├── App.jsx
├── index.css
└── main.jsx
Paso 2: Instalar React Router DOM
Abrimos una terminal en nuestro proyecto 1-peliculas y ejecutamos:
bash
cd 1-peliculas
yarn add react-router-dom
O con npm:
bash
npm install react-router-dom
Paso 3: Crear componentes básicos
MovieCard.jsx (Componente individual de película)
jsx
export function MovieCard() {
return (
<div>
<h1>Movie Card</h1>
{/* Aquí irá la información de cada película */}
</div>
);
}
ContextMovieCard.jsx (Contenedor de películas)
jsx
export function ContextMovieCard() {
return (
<div>
<h1>Context Movie Card</h1>
{/* Aquí irán múltiples MovieCard */}
</div>
);
}
Paso 4: Crear la Landing Page
LandingPage.jsx
jsx
import { ContextMovieCard } from "../components/ContextMovieCard";
export function LandingPage() {
return (
<div>
<ContextMovieCard />
</div>
);
}
Paso 5: Configurar el Router
routes.jsx
jsx
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { LandingPage } from "../pages/LandingPage";
export function AppRoutes() {
return (
<Router>
<Routes>
{/* Ruta principal */}
<Route path="/" element={<LandingPage />} />
{/* Ruta para detalles de película */}
<Route path="/movie/:id" element={<div>Detalle de Película</div>} />
{/* Ruta 404 */}
<Route path="*" element={<div>Página no encontrada</div>} />
</Routes>
</Router>
);
}
export default AppRoutes;
Paso 6: Integrar el Router en App.jsx
App.jsx actualizado
jsx
import './App.css';
import AppRoutes from './routers/routes';
function App() {
return (
<div className="App">
<header>
<h1 className='title'>Películas 🎬</h1>
</header>
<main>
<AppRoutes />
</main>
</div>
);
}
export default App;
Paso 7: Mejorar los componentes con datos reales
Actualizando ContextMovieCard.jsx
jsx
import { useEffect, useState } from 'react';
import { get } from '../data/httpClient';
import { MovieCard } from './MovieCard';
import './ContextMovieCard.css';
export function ContextMovieCard() {
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchMovies = async () => {
try {
setLoading(true);
const data = await get('discover/movie');
setMovies(data.results.slice(0, 12)); // Limitar a 12 películas
} catch (err) {
setError('Error al cargar las películas');
console.error(err);
} finally {
setLoading(false);
}
};
fetchMovies();
}, []);
if (loading) {
return (
<div className="loading-container">
<div className="spinner"></div>
<p>Cargando películas...</p>
</div>
);
}
if (error) {
return (
<div className="error-container">
<p>❌ {error}</p>
<button onClick={() => window.location.reload()}>
Reintentar
</button>
</div>
);
}
return (
<div className="context-movie-card">
<div className="movies-grid">
{movies.map((movie) => (
<MovieCard
key={movie.id}
movie={movie}
/>
))}
</div>
</div>
);
}
Actualizando MovieCard.jsx
jsx
import { Link } from 'react-router-dom';
import './MovieCard.css';
export function MovieCard({ movie }) {
return (
<Link to={`/movie/${movie.id}`} className="movie-card-link">
<div className="movie-card">
<img
src={`https://image.tmdb.org/t/p/w300${movie.poster_path}`}
alt={movie.title}
className="movie-poster"
/>
<div className="movie-info">
<h3>{movie.title}</h3>
<div className="movie-details">
<span className="rating">
⭐ {movie.vote_average.toFixed(1)}
</span>
<span className="year">
{new Date(movie.release_date).getFullYear()}
</span>
</div>
</div>
</div>
</Link>
);
}
Paso 8: Crear archivos CSS para los componentes
ContextMovieCard.css
css
.context-movie-card {
padding: 2rem 0;
}
.movies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 2rem;
padding: 0 1rem;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
gap: 1rem;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(255, 215, 0, 0.3);
border-top: 5px solid #ffd700;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-container {
text-align: center;
padding: 3rem;
color: #ff6b6b;
}
.error-container button {
margin-top: 1rem;
padding: 0.5rem 1rem;
background: #ffd700;
border: none;
border-radius: 5px;
color: #000;
cursor: pointer;
font-weight: bold;
}
.error-container button:hover {
background: #ffed4e;
}
MovieCard.css
css
.movie-card-link {
text-decoration: none;
color: inherit;
}
.movie-card {
background: #16213e;
border-radius: 10px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
height: 100%;
}
.movie-card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4);
}
.movie-poster {
width: 100%;
height: 300px;
object-fit: cover;
}
.movie-info {
padding: 1rem;
}
.movie-info h3 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #ffffff;
}
.movie-details {
display: flex;
justify-content: space-between;
align-items: center;
}
.rating {
color: #ffd700;
font-weight: bold;
display: flex;
align-items: center;
gap: 0.3rem;
}
.year {
color: #a0a0a0;
font-size: 0.9rem;
}
/* Responsive */
@media (max-width: 768px) {
.movies-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
}
.movie-poster {
height: 225px;
}
}
Paso 9: Actualizar App.css para incluir el main
css
/* Estilos existentes... */
main {
min-height: calc(100vh - 200px);
padding-bottom: 3rem;
}
/* Estilos para el título */
.title {
text-align: center;
font-size: 3rem;
margin: 2rem 0;
color: #ffd700;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.3);
}
/* Estilos para el header */
header {
padding: 1rem;
background: linear-gradient(135deg, #16213e 0%, #0f3460 100%);
border-bottom: 3px solid #ffd700;
margin-bottom: 2rem;
}
Paso 10: Crear página de detalle de película
Crear MovieDetailPage.jsx
jsx
import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { get } from '../data/httpClient';
import './MovieDetailPage.css';
export function MovieDetailPage() {
const { id } = useParams();
const [movie, setMovie] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchMovie = async () => {
try {
setLoading(true);
const data = await get(`movie/${id}`);
setMovie(data);
} catch (err) {
setError('Error al cargar los detalles de la película');
console.error(err);
} finally {
setLoading(false);
}
};
fetchMovie();
}, [id]);
if (loading) {
return (
<div className="loading">
<p>Cargando detalles...</p>
</div>
);
}
if (error || !movie) {
return (
<div className="error">
<p>{error || 'Película no encontrada'}</p>
</div>
);
}
return (
<div className="movie-detail">
<div className="movie-backdrop">
<img
src={`https://image.tmdb.org/t/p/original${movie.backdrop_path}`}
alt={movie.title}
/>
<div className="backdrop-overlay"></div>
</div>
<div className="movie-content">
<div className="movie-poster">
<img
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={movie.title}
/>
</div>
<div className="movie-info">
<h1>{movie.title}</h1>
<div className="movie-meta">
<span className="rating">
⭐ {movie.vote_average.toFixed(1)}/10
</span>
<span className="runtime">
🕒 {movie.runtime} min
</span>
<span className="release-date">
📅 {new Date(movie.release_date).toLocaleDateString()}
</span>
</div>
<div className="genres">
{movie.genres.map(genre => (
<span key={genre.id} className="genre-tag">
{genre.name}
</span>
))}
</div>
<div className="overview">
<h2>Sinopsis</h2>
<p>{movie.overview}</p>
</div>
<div className="additional-info">
<div className="info-item">
<strong>Presupuesto:</strong>
<span>${movie.budget.toLocaleString()}</span>
</div>
<div className="info-item">
<strong>Ingresos:</strong>
<span>${movie.revenue.toLocaleString()}</span>
</div>
</div>
</div>
</div>
</div>
);
}
Actualizar routes.jsx para incluir la nueva página
jsx
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { LandingPage } from "../pages/LandingPage";
import { MovieDetailPage } from "../pages/MovieDetailPage";
export function AppRoutes() {
return (
<Router>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/movie/:id" element={<MovieDetailPage />} />
<Route path="*" element={<div>Página no encontrada</div>} />
</Routes>
</Router>
);
}
export default AppRoutes;
Resultado final
¡Excelente! Ahora tienes una aplicación con:
✅ Router funcional con React Router DOM v6
✅ Dos rutas principales: / (lista de películas) y /movie/:id (detalle)
✅ Componentes reutilizables y bien organizados
✅ Navegación entre páginas con <Link>
✅ Manejo de estados de carga y error
✅ Diseño responsive y atractivo
Próximas mejoras posibles
Agregar más rutas: Búsqueda, favoritos, categorías
Implementar navegación: Menú de navegación global
Mejorar UX: Loading skeletons, transiciones suaves
Agregar funcionalidades: Búsqueda en tiempo real, filtros avanzados
Consejo: Siempre mantén tu estructura de carpetas organizada. Esto te ayudará mucho cuando tu aplicación crezca y tengas muchos componentes y páginas.
¡Ahora tu aplicación de películas tiene navegación profesional!
Comentarios
Publicar un comentario