Data Fetching with React Query and TypeScript using the Movie Database Api

donsmog

Olufeyimi Samuel

Posted on May 9, 2023

Data Fetching with React Query and TypeScript using the Movie Database Api

A few years back when I started learning ReactJS, I was introduced to data fetching using React Hooks, specifically the useState and useEffect hooks. But every time the page paints/loads, the data is been re-fetched. This suffice for a while until I found a better way of handling data fetching using React Query. Data fetching with React Query was a game changer for me.

This article is targeted towards beginners like I was few years ago. To illustrate the usage of React Query better, we will be creating a single page movie page using the movie database api.

Please note: this article won’t cover how to setup an account on the movie database (TMDB). If needed, I will write on that in another article. For this project, I already have my api key from TMDB and that will be used in this example. To learn more about TMDB, kindly check out their official website

So let’s jump in…
To begin, let’s create a new React Project using Vite.

Open your VSCode or any other code editor, open the terminal and type in yarn create vite or npm create vite@latest then follow the prompts. I will be using React with TypeScript in this example.
You can checkout the official documentation page for more information on Vite.

Next, let’s install some dependencies. In your terminal, type in yarn add axios @tanstack/react-query. Then run yarn dev to start the project.

As stated above, I’ve setup my TMDB account. For this illustration, we will be using the discover movies api. This api have about 30 params for filtering. However, we will only use ‘sort_by’ and ‘page’ params in addition to the compulsory ‘api_key’. Our endpoint will look like this https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=${my_api_key}&page=${page}
The page will be controlled to allow for fetching of the next page.

Just to speed things up, I’ve implemented a simple movie page using the regular useState and useEffect. I’m displaying the movie title, movie thumbnail, movie rating and movie description which is seen on hover of each movie card.
The project structure looks something like this:

Project directory

As seen in the directory above, there are 4 major files in the src folder, App.tsx, main.tsx, movie.tsx and index.css. I modified App.tsx and index.css and created a new movie.tsx. The main.tsx is as it were for now.

Using our useState and useEffect, we can fetch the data within the App.tsx component as seen below.

import { useEffect, useState } from "react";
import Movie from "./movie";

export interface Movies {
  id?: number;
  title: string;
  poster_path: string;
  overview: string;
  vote_average: number;
}

const my_api_key = import.meta.env.VITE_API_KEY;

function App() {
  const [loading, setLoading] = useState<boolean>(false);
  const [movies, setMovies] = useState<Movies[]>([]);
  const [page, setPage] = useState<number>(1);

  const FeaturedApi = `https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=${my_api_key}&page=${page}`;

  const getMovies = (API: string) => {
    setLoading(true);
    fetch(API)
      .then((res) => res.json())
      .then((data) => {
        setMovies(data.results);
        setLoading(false);
      });
  };

  useEffect(() => {
    getMovies(FeaturedApi);
  }, [FeaturedApi]);

  return (
    <div className="movie_app">
      <h1 className="header">
        Simple Movie App with React Query and TypeScript
      </h1>

      <div className="movie-container">
        {loading ? (
          <h1 className="loading">Loading...</h1>
        ) : (
          <>
            {movies.length > 0 &&
              movies.map((movie) => <Movie key={movie.id} {...movie} />)}
          </>
        )}
      </div>

      <div className="pagination">
        <button
          onClick={() => setPage((prev) => prev - 1)}
          disabled={page === 1 ? true : false}
        >
          Prev
        </button>
        <button
          onClick={() => setPage((prev) => prev + 1)}
          disabled={page === 100 ? true : false}
        >
          Next
        </button>
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In the snippet above, the constant my_api_key is gotten from TMDB.
The movie.tsx component looks like this:

import { Movies } from "./App";

const Movie = ({ title, poster_path, overview, vote_average }: Movies) => {
  const ImagesApi = "https://image.tmdb.org/t/p/w1280";
  const setVoteClass = (vote: number) => {
    if (vote >= 8) {
      return "green";
    } else if (vote >= 6) {
      return "orange";
    } else {
      return "red";
    }
  };

  return (
    <div className="movie">
      <img
        src={
          poster_path
            ? ImagesApi + poster_path
            : "https://indianfolk.com/wp-content/uploads/2018/10/Movie.jpg"
        }
        alt={title}
      />
      <div className="movie-info">
        <h3>{title}</h3>
        <span className={`tag ${setVoteClass(vote_average)}`}>
          {vote_average}
        </span>
      </div>
      <div className="movie-over">
        <h2>Overview:</h2>
        <p>{overview}</p>
      </div>
    </div>
  );
};

export default Movie;
Enter fullscreen mode Exit fullscreen mode

What we've simply done is declared a getMovies function that takes in 1 parameter which is the api. We then use the regular fetch function to call the api. Once there's a response, we convert to JSON and then store in the movies state. At the beginning of the call, we change the loading state to true and at the end of the process, we change the loading state back to false.

Within the body of the component, we display a loading message while data fetching is ongoing then we map out the new updated movies array once the data fetching process is completed.

We also have a pagination part with 2 buttons for increasing and decreasing the page state.

This implementation works perfectly fine. However, it is data consuming. Every time we increment or decrement pages, we are calling the endpoint over and over again. Also, we get a blink "Loading..." message every time the page changes.

What if we could cache or temporarily store the previously fetched data and only update it in the background when there's a new update? This and many more is what React Query allows us to do.

What exactly is React Query?
React Query is a JavaScript library that allows you fetch, cache, synchronize and update server state in your web application. In simple terms, that means with RQ, you can fetch data, temporarily store the data, update the data in the background and synchronize your display with the most updated data. You can read more here.

The fun part of RQ is that it is so easy to include in your codebase. We will cover that part right away.

In our main.tsx, we will import 2 functions from react-query and initialize it. We do:

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
Enter fullscreen mode Exit fullscreen mode

We then create a client by doing

const queryClient = new QueryClient();
Enter fullscreen mode Exit fullscreen mode

Then we wrap our root App component with the QueryClientProvider like this

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

Next, we go back into our App.tsx and import 2 things

import axios from "axios";
import { useQuery } from "@tanstack/react-query"; 
Enter fullscreen mode Exit fullscreen mode

We then modify our getMovies function to be

const getMovies = async () => {
    return await axios.get(FeaturedApi).then((res) => res.data.results);
  };
Enter fullscreen mode Exit fullscreen mode

We can now make use of useQuery as such

const { isLoading, data } = useQuery<Movies[]>({
    queryKey: ["movies", page],
    queryFn: getMovies,
  });
Enter fullscreen mode Exit fullscreen mode

What this will do is cache each page with a unique key, in this case as movies-(page). E.g. When page is 1, the unique key is movies-1. This way, you won't have to refetch page 1 as its already cached. Its important to note that the default cache time is 5mins, meaning your data can persist for 5 minutes. However,this option can be changed.

Going forward, we can remove the loading state and the movie state in the previous App.tsx component.

In the jsx, we update the map section to be

{isLoading ? (
          <h1 className="loading">Loading...</h1>
        ) : (
          <>
            {(data ?? []).length > 0 &&
              (data ?? []).map((movie) => <Movie key={movie.id} {...movie} />)}
          </>
        )}
Enter fullscreen mode Exit fullscreen mode

And there you have it. You can view the demo of what we've done here

Since I found React Query, it has been my go to data fetching package. There are many other things we can do with React Query, but we won't touch on those in this article.

If this was helpful, do leave a like and if there's any part not clear, do drop a comment and I will be so glad to help.

Cheers!!!

💖 💪 🙅 🚩
donsmog
Olufeyimi Samuel

Posted on May 9, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related