DEV Community

Cover image for Movie App
Rounak Agarwal
Rounak Agarwal

Posted on

Movie App

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
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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();
}, []);
Enter fullscreen mode Exit fullscreen mode

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);
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)