Getting Back to React After a DSA Detour: Building a Movie App
For the last few months, my coding time has almost entirely gone into DSA prep and a couple of courses. Good for interviews, not so good for actually building things. At some point I realized I hadn't shipped a single React project in a while, and my "practical" React muscle was getting rusty so I built a small Movie App to shake off the dust.
Repo: Rounakag16/Movie-App
Live Preview: Website
The idea
Nothing fancy, I wanted something scoped enough to finish in a few sessions, but with enough moving parts to actually touch the concepts I wanted to refresh: routing, context, side effects, and talking to an external API. A movie browser fit perfectly:
- Browse popular movies on load
- Search for any movie by title
- Add/remove movies from a Favourites list that persists across refreshes
- Basic client-side routing between Home and Favourites
Tech stack
- React 19 for the UI
- Vite for the dev server and build
- React Router v7 for navigation
- TMDB API (The Movie Database) as the data source
Nothing exotic on purpose — the goal was reps, not novelty.
How it's structured
src/
├── assets/
├── components/
│ ├── MovieCard.jsx
│ └── Navbar.jsx
├── contexts/
│ └── MovieContext.jsx
├── css/
├── pages/
│ ├── Home.jsx
│ └── Favourites.jsx
├── services/
│ └── api.js
├── App.jsx
└── main.jsx
Pretty standard separation: pages for routes, components for reusable pieces, services for API calls, and contexts for shared state.
The parts that actually exercised the concepts I was rusty on
1. Talking to an API cleanly
services/api.js just wraps the two TMDB endpoints I needed, using env vars for the key and base URL instead of hardcoding them:
const apiKey = import.meta.env.VITE_API_KEY;
const baseUrl = import.meta.env.VITE_BASE_URL;
export const getPopularMovies = async () => {
const response = await fetch(`${baseUrl}/movie/popular?api_key=${apiKey}`);
const data = await response.json();
return data.results;
};
export const searchMovies = async (query) => {
const response = await fetch(
`${baseUrl}/search/movie?api_key=${apiKey}&query=${encodeURIComponent(query)}`,
);
const data = await response.json();
return data.results;
};
Keeping fetch logic out of components made the Home page a lot easier to reason about — it just calls a function and handles loading/error state, it doesn't care how the request is built.
2. useEffect + loading/error state, the classic pattern
Home.jsx fetches popular movies on mount, and re-fetches on search:
useEffect(() => {
const loadPopularMovies = async () => {
try {
const popularMovies = await getPopularMovies();
setMovies(popularMovies);
} catch (err) {
setError("Failed to load movies...");
} finally {
setLoading(false);
}
};
loadPopularMovies();
}, []);
Simple, but it's exactly the kind of thing that gets fuzzy in your head after a stretch of pure algorithms — async state handling, guarding against empty searches, not re-triggering a search while one's already loading.
3. Context API for favourites, instead of prop-drilling
This was the part I most wanted to practice. MovieContext.jsx holds the favourites array, syncs it to localStorage, and exposes helpers:
const MovieProvider = ({ children }) => {
const [favourites, setFavourites] = useState([]);
useEffect(() => {
const stored = localStorage.getItem("favourites");
if (stored) setFavourites(JSON.parse(stored));
}, []);
useEffect(() => {
localStorage.setItem("favourites", JSON.stringify(favourites));
}, [favourites]);
const addToFavourites = (movie) => setFavourites((prev) => [...prev, movie]);
const removeFromFavourites = (movieId) =>
setFavourites((prev) => prev.filter((movie) => movie.id !== movieId));
const isFavourite = (movieId) => favourites.some((movie) => movie.id === movieId);
// ...
};
Then MovieCard just calls useMovieContext() to check/toggle favourite status, with zero prop passing between Home/Favourites and the card component. Wrapping the whole app in <MovieProvider> inside App.jsx and letting any nested component reach in was a nice reminder of why Context exists in the first place.
4. Routing
App.jsx keeps this dead simple:
<MovieProvider>
<Navbar />
<main className="main-content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/favourites" element={<Favourites />} />
</Routes>
</main>
</MovieProvider>
Two routes, one shared layout, favourites accessible from anywhere via the navbar.
What this exercise actually did for me
It wasn't about building something groundbreaking, TMDB movie apps are a well-worn tutorial project for a reason. The point was forcing myself to go from "I remember how Context works" to actually wiring up a Provider, actually handling a useEffect cleanup-less async call, actually deciding where API logic should live. DSA sharpens problem-solving, but it doesn't keep your "shape an app out of components" instincts warm. This did.
If you're in a similar spot, deep in interview prep and feeling disconnected from actually building I'd recommend picking something small like this. Pick a project that's boring enough to finish in a weekend, and let the boredom do the work of re-drilling fundamentals.
Repo again if you want to poke around or fork it: Rounakag16/Movie-App
Top comments (0)