Proyecto Final: App Películas Avanzada
El proyecto final integra todo el ecosistema React profesional: Context + useReducer para el estado global de favoritos, custom hooks para la lógica de fetch reutilizable, React.memo para que las tarjetas de películas no se re-rendericen innecesariamente, Error Boundaries para que un fallo en la grilla no rompa el resto de la app, React Router para navegación multi-página, y tests con RTL para garantizar que los flujos principales funcionan. Esta arquitectura es exactamente la que encontrarás en proyectos de empresas reales.
La clave de esta arquitectura es la separación de responsabilidades: custom hooks manejan datos y lógica asíncrona, componentes solo manejan UI, Context maneja estado global, y Router maneja la navegación.
Cuando cada pieza tiene una responsabilidad única, el código es más fácil de mantener (cambias un hook sin tocar la UI), testear (pruebas el hook aislado) y escalar (agregas features sin romper lo existente). Con este proyecto, tienes las bases de un React developer profesional.
// Arquitectura final del proyecto
// context/MovieContext.jsx - Estado global con useReducer
import { createContext, useContext, useReducer } from "react";
const MovieCtx = createContext(null);
function movieReducer(state, action) {
switch (action.type) {
case "ADD_FAV":
return { ...state, favorites: [...state.favorites, action.movie] };
case "REMOVE_FAV":
return { ...state, favorites: state.favorites.filter(m => m.id !== action.id) };
case "SET_QUERY":
return { ...state, query: action.query };
default: return state;
}
}
export function MovieProvider({ children }) {
const [state, dispatch] = useReducer(movieReducer, { favorites: [], query: "" });
return <MovieCtx.Provider value={{ state, dispatch }}>{children}</MovieCtx.Provider>;
}
export const useMovies = () => useContext(MovieCtx);
// hooks/useFetch.js - Custom hook genérico
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url).then(r => r.json()).then(setData).finally(() => setLoading(false));
}, [url]);
return { data, loading };
}
// components/MovieCard.jsx - Memoizado
const MovieCard = React.memo(({ movie }) => {
const { dispatch } = useMovies();
return (
<div className="card">
<img src={movie.poster} alt={movie.title} loading="lazy" />
<h3>{movie.title}</h3>
<button onClick={() => dispatch({ type: "ADD_FAV", movie })}>
❤️ Favorito
</button>
</div>
);
});
// App.jsx - Compone todo con Error Boundaries
export default function App() {
return (
<MovieProvider>
<ErrorBoundary fallback={<p>Error cargando películas</p>}>
<Suspense fallback={<p>Cargando...</p>}>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/favoritos" element={<Favoritos />} />
<Route path="/pelicula/:id" element={<Detalle />} />
</Routes>
</Router>
</Suspense>
</ErrorBoundary>
</MovieProvider>
);
}
Practica esta lección
Resuelve los ejercicios del curso de React en la app, gratis y con corrección automática.
Empezar a practicar