Movie Explorer, "The Challenge"

A Macbook Pro in the dark

In this post, I’ll be sharing my take from reciving the challenge document, to production, this includes how I approached the project, and what made me to choose a solution over another. stick with me, you can learn some pretty cool stuff!

The Challenge

The challenge was to build a movie explorer app, where users can see popular movies, and also search for them. The main problem here is the Data Fetching part, which can be so tricky, since we have a bunch of data, and i really mean a bunch of data. we have around 45,886 total pages, with over 917,716 movies, and that can be increased 🥶. so we have to provide a good user experience, while also being performant. and consider all type of users, from the ones with fancy laptops, and 5G fiber networks, to the ones with low bandwidth, slow internet, or just a bad connection. So how do we tackle this problem?

The Brainstorming Session 🧠

Give me six hours to chop down a tree and I will spend the first four sharpening the axe. ― Abraham Lincoln

I always start any project/task with a the planning first, which very important to me, i never touch vscode until i have a clear vesion in my eyes. i see folks start coding right away after they got the problemset, and they don’t have a clear vision of what they want to build.

So, i started with all the obvious problems, and the hidden ones, and here’s what i came up with:

  • We have a huge dataset, and we want the users to be able to look at anything in inside it.
  • We will have a lot of images, which can be heavy for loading, and we want to optimize it.
  • We have to make api calls just when we really need it, so we have to use some caching mechanism.

My Plan

We are going to use pagination for the data fetching, and only request a small number of movies each time the user requests it. for the popular movies, we will design an infinite scrolling experience, i got inspired by Youtube, and Instagram for this. so basically, we will fetch the first page of movies by default, and display them, and when the user scrolls down, and reach the bottom of the page, we will add a spinner right there that says “Catch me if you can” hh. and then we will fetch the next page of movies, and display it, and so on and so forth. And for the search part, we will use a search bar, and when the user types something, we will debounce the query to avoid making too many requests, and then make an api call, and display the results in the same way as the popular movies.

And for the images, we will simply use lazy loading, and resizing the image using Next Image.

And most importantly for data fetching and caching, we will use React Query for this. and we can combine it with custom hooks, so that we have a complete state management solution for our app.

The Coding Part

let’s start our app by initializing a new nextjs app, and install shadcn for the UI. and then install the react query package, and we will be using axios as our http client. and finally debouce from lodash, for the debouncing part.

npx create-next-app movie-explorer
cd movie-explorer
npm i @shadcn/ui @tanstack/react-query axios lodash.debounce

And then we will grab an api key from themoviedb.

Now let’s create a clean folder structure for our app.

- movie-explorer
  - app
  - components
    - ui  # for our shadcn ui
    - shared # for the shared components
    - explorer # for the explorer components
  - utils
    - api.js # provides an instance of axios pre-configured with the api key and base url
    - ...other utils
  - hooks
    - useMovieQueries.ts # handles all the movie queries, and provides a global state.
  - types
    - movie.ts # for the movie types
  - providers
   - theme-provider.tsx # for the theme toggling

I will not focus on the ui part, since you can implement it yourself, but i will focus on the business logic part, and the data fetching part.

Let’s take a look at the hooks/useMovieQueries.ts file, and here’s what it does:

import { moviesApi } from '@/lib/api';
import { MovieDetails, MoviesResponse } from '@/types/movie';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import debounce from 'lodash.debounce';
import { useCallback, useMemo, useState } from 'react';

export const useMovies = () => {
  const [query, setQuery] = useState<string>('');
  const [debouncedQuery, setDebouncedQuery] = useState<string>('');

  const apiEndpoint = useMemo(() => (debouncedQuery ? `search/movie?query=${query}` : 'movie/popular'), [debouncedQuery]);

  // Am using the useCallback hook to cache the func definition
  const debounceQuery = useCallback(
    debounce((q: string) => setDebouncedQuery(q), 500),
    []
  );

  const onQueryChange = (value: string) => {
    setQuery(value);
    debounceQuery(value);
  };

  const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery<MoviesResponse>({
    queryKey: ['movies', debouncedQuery],
    initialPageParam: 1,
    queryFn: async ({ pageParam }) => {
      const response = await moviesApi.get<MoviesResponse>(apiEndpoint, {
        params: { page: pageParam }
      });

      return response.data;
    },
    getNextPageParam: (lastPage) => {
      return lastPage.page < lastPage.total_pages ? lastPage.page + 1 : undefined;
    },
    staleTime: 5 * 60 * 1000
  });

  return {
    movies: data?.pages.flatMap((movies) => movies.results) ?? [],
    isLoading,
    isError,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    query,
    onQueryChange
  };
};

export const useMovieDetails = () => {
  const [movieId, setMovieId] = useState<number | null>(null);

  const { data, isError, isLoading } = useQuery<MovieDetails>({
    queryKey: ['movieDetails', movieId],
    queryFn: async () => {
      const response = await moviesApi.get<MovieDetails>(`movie/${movieId}`);
      console.log(response.status);
      return response.data;
    },
    enabled: !!movieId,
    staleTime: 5 * 60 * 1000
  });

  return {
    movieDetails: data,
    isLoading,
    isError,
    movieId,
    setMovieId
  };
};

Let’s break that down, and see what’s going on here.

First, we are using the useInfiniteQuery which is a hook that allows us to fetch data in an paginated way. It takes a few parameters, like the query key, the initial page param, the query function, and the get next page param function, and we are setting the staleTime to 5 minutes since the data isn’t changing that often.

So here we are fetching the movies either by the popular movies or by the search results. if there’s no query, (so the user is just browsing the popular movies), we are using the popular movies as the source of truth, and if there is a query, we are using the search results as the source of truth. we are using the debounce function from lodash to debounce the query, so that we don’t make too many requests, and we are also using the useCallback hook to cache the debounce function so it’s not recreated on every render.

and then we are exporting the movies array, and the other useful information like isLoading, isError, fetchNextPage, hasNextPage, and isFetchingNextPage. so that we can use them in our components.

and then we are using the useQuery hook, which is a hook that allows us to fetch data once, and then cache it for future use. it takes a few parameters, like the query key, the query function, and the enabled state. so if the user is searching for a movie, we want to fetch the movie details once, and cache it for future use.

So what react query really does is, it first fetch the data, and cache it, if we request it again, and it’s already cached, it will return the cached data, and if the data is stale, it will fetch it again, and update the cache. very powerful isnt’it? 💪🏼.

Here’s an explanation of the react query workflow: react query workflow

And for useMovieDetails hook is very simmilar, we are using the movieId as a reference for the cache key, so if it’s already cached, we will return the cached data, and if the data is stale, it will fetch it again, and update the cache.

The Result

demo1

demo2

That’s it!

I hope you enjoyed this post, and you learned something new.

Subscribe to My Newsletter

I do post sometimes. All the latest posts directly in your inbox.