Building a Movie App with React Hooks and Cloudflare Workers
Tags: react, hooks, cloudflare, workers
Published: 2019-08-31 21:15:37

---
title: Building a Movie App with React Hooks and Cloudflare Workers
published: true
description: This is a tutorial on how to use React Hooks to build the UI for a movie app and Cloudflare Workers (serverless or cloud functions) for the proxy API.
tags: React, Hooks, Cloudflare, Workers
cover_image: https://thepracticaldev.s3.amazonaws.com/i/fqhhsxynqag05xshvjpb.png
---

Hey there! This is my first tutorial here. My name is Jean Gérard and I'm a developer based out Port-au-Prince, Haiti.

So I've been working on this Spotify/SoundCloud clone app called `MP3 Pam` for a couple of months now. React on the front-end and Laravel on the back-end for the API. I use React Hooks and Cloudflare Workers quite a bit and I thought it would be good to share some of the things I've learned on the internet. ;)

So what are we going to build? A [movie app](https://movie-app-workers.jgb.solutions/) ([movie-app-workers.jgb.solutions](https://movie-app-workers.jgb.solutions/)) that allows you to search for any movies, series or TV shows. We'll make use of the [OMDb API](http://www.omdbapi.com/). It's free for up to 1000 requests per day. We will use `Cloudflare Workers` to protect our API key, do some rerouting, and a lot of caching. That will allows us to bypass their 1000 requests per day limit and get nice API urls for free, since Cloudflare Workers is free for up to *100 000* requests per day.

So what is React? React is a JavaScript library (can also be called a framework) that allows you to create better UI (user interface) for web (React.js) and mobile (React Native).

What about this React Hooks thing? Yeah, so according to the official docs `Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class. Hooks are backwards-compatible. This page provides an overview of Hooks for experienced React users. This is a fast-paced overview.` In other words, Hooks will allow us to use just functional components and add state management and lifecycle to them without the need to use `class`. And that's a good thing because they seem to offer [many advantages](https://reactjs.org/docs/hooks-intro.html#motivation) over traditional `React class component`.

And about that last buzzword in our list, Cloudflare Workers? Their docs state that `Cloudflare Workers provides a lightweight JavaScript execution environment that allows developers to augment existing applications or create entirely new ones without configuring or maintaining infrastructure.` In other words, we can use it to do what traditional servers do, only we won't need to manage or even pay for them. Yay!

Okay now to complete this tutorial you need some React knowledge, [Node.js](https://nodejs.org/en/), a [code editor](https://code.visualstudio.com/), and a [browser](https://www.google.com/chrome/) installed on your machine.

To follow along you can clone the starter files ([client](https://github.com/jgb-solutions/movie-app-workers-client), [api](https://github.com/jgb-solutions/movie-app-workers-api)) and `git checkout starter` to access the `starter` branch or use [create-react-app](https://github.com/facebook/create-react-app) and [wrangler](https://github.com/cloudflare/wrangler) to create a new react project and a workers project respectively.

`cd` into the `client` folder and run `yarn`. That command will install the node dependencies needed to run our app while developing locally. While you're at it pull `lodash-es` with `yarh add lodash-es`. We will make use of its `get` method to access object properties without getting errors when the object or any of the property's parent properties are undefined.

I already imported the Bootstrap 4 CSS in the `App.css` file to get us started with some basic styling since that's not the main topic of the tutorial.

Once everything is installed run `yarn start` and you should see a blank page. That's right. We haven't done anything fancy yet.

Now We need to create 2 files in the `src` folder: `MovieList.js` and `useMovies.js`.

`MovieList.js` will be responsible for displaying the search input and the the list of movies (series, tv shows) and also load more items from the API.

Go ahead a paste this bit of code in it and I will explain what it does.

```js
import React from 'react';
import { get } from 'lodash';

import useMovies from './useMovies';
import logo from './logo.svg';

let debounceSearch;

function MovieList() {
  const [
    movies,
    setSearchTerm,
    isLoading,
    canLoadMore,
    fetchMovies,
    lastSearchTerm,
    setMovies,
  ] = useMovies()

  const handleSearch = event => {
    const searchTerm = event.target.value.trim();

    if (searchTerm.length > 2) {
      clearTimeout(debounceSearch)
      // do search
      debounceSearch = setTimeout(() => {
        setSearchTerm(searchTerm);
      }, 500);
    } else {
      setMovies([]);
    }
  }

  return (
      <div className="col-sm-8 offset-sm-2">
        <header>
          <h1>
            <img src={logo} alt='Movie App Workers' className='logo' f/>
            Movie App
          </h1>
        </header>
        <form>
          <div className="input-group">
            <input type="text"
              className="form-control"
              placeholder="Search any movie, series or TV Shows"
              onChange={handleSearch}
            />
          </div>
        </form>
        <br />
        {isLoading && <h2>Search Loading...</h2>}
        <div className="row">
          {movies.length ? (
            movies.map(movie => {
              const title = get(movie, 'Title', `No Title`);
              const movieId = get(movie, 'imdbID')
              let poster = get(movie, 'Poster');
              if (!poster || poster === 'N/A') {
                poster = `https://dummyimage.com/300x448/2c96c7/ffffff.png&text=No+Image`;
              }
              const type = get(movie, 'Type', `undefined`);
              const year = get(movie, 'Year', `undefined`);

              return (
                <div key={movieId} className="col-sm-6 mb-3">
                  <div className="row">
                    <div className="col-7">
                      <img src={poster} alt={title} className='img-fluid' />
                    </div>
                    <div className="col-5">
                      <h3 className='movie-title'>{title}</h3>
                      <p>Type: {type}.<br /> Year: {year}</p>
                    </div>
                  </div>
                </div>
              )
            })
          ) : lastSearchTerm.length > 2 ? <div className="col-12"><h2>No Movies Found</h2></div> : null}
        </div>
        {!!movies.length && canLoadMore && (
          <button
            className='btn btn-primary btn-large btn-block'
            onClick={fetchMovies}>
            Load More
          </button>
        )}
        <br />
        <br />
        <br />
      </div>
    )
}

export default MovieList;
```
This is a huge piece of code, I will admit it. So what is happening here is that we begin by creating regular functional component.
```js
import React from 'react';
import { get } from 'lodash';

import useMovies from './useMovies';
import logo from './logo.svg';
```
We import `react`, the `get` method from `lodash`, the `useMovies` hook (that we will fill in a second) and the default react logo that we use next to the title of the app.

Next we have
```js
let debounceSearch;
```
this variable will hold a timer id that we use to delay the call to the API by not calling an API for every key stroke but rather wait for half a second (`500 milliseconds`) to hit it.

The next interesting bit is:
```js
 const [
    movies,
    setSearchTerm,
    isLoading,
    canLoadMore,
    fetchMovies,
    lastSearchTerm,
    setMovies,
  ] = useMovies()
```
Here we call our `useMovies` hook that gives us a list of `movies`, a `setSearchTerm` method to set the value for which we want to search, `canLoadMore` is a boolean that tells us whether we can load more movies or not and thus we will show or hide the load more button, `fetchMovies` is the method we will call when we want new movies, `lastSearchTerm` is a string that stores that last value that we successfully had a result for and thus let us compare it to the current string value we want to search for to see whether we want to make a new search and clear the list we have or append to it, `setMovies` allows to empty the list of movies when the length of the characters is less than 3.

Next we have:
```js
const handleSearch = event => {
    const searchTerm = event.target.value.trim();

    if (searchTerm.length > 2) {
      clearTimeout(debounceSearch)
      // do search
      debounceSearch = setTimeout(() => {
        setSearchTerm(searchTerm);
      }, 500);
    } else {
      setMovies([]);
    }
  }
```
Here we use the input change event to access that value of the text, trim it for white spaces, use the `setTimeOut` function to delay the call for half a second, otherwise we set the list to an empty array.

Now:
```js
const title = get(movie, 'Title', `No Title`);
const movieId = get(movie, 'imdbID')
let poster = get(movie, 'Poster');
if (!poster || poster === 'N/A') {
 poster = `https://dummyimage.com/300x448/2c96c7/ffffff.png&text=No+Image`;
}
const type = get(movie, 'Type', `undefined`);
const year = get(movie, 'Year', `undefined`);
```
We use `get` from `lodash` to avoid errors with undefined objects and properties, provide default values for texts and the poster and we store those values in new variables that we use in our `JSX` returned by the function.

```js
{!!movies.length && canLoadMore && (
  <button
   className='btn btn-primary btn-large btn-block'
   onClick={fetchMovies}>
   Load More
  </button>
)}
```
In this bit of code, first we cast the `movies.length` value to a boolean, and if that's true and if we can load more we display the load more button that itself calls the `fetchMovies` method.

And that is a quick tour of the code. I'm hoping you can understand the rest. Otherwise hit me on [Twitter here](https://twitter.com/jgb_solutions).

Now paste this code in your `useMovies.js` file:
```js
import { useState, useEffect } from 'react';

function useMovies() {
  const [movies, setMovies] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [canLoadMore, setCanLoadMore] = useState(false);
  const [page, setPage] = useState(1)
  const [searchTerm, setSearchTerm] = useState(undefined)
  const [lastSearchTerm, setLastSearchTerm] = useState('')

  const fetchMovies = async () => {
    setIsLoading(true);
    if (searchTerm !== lastSearchTerm) {
      setPage(1);
      setMovies([]);
    }

    try {
      const response = await fetch(
        `https://movie-api-app.jgb.solutions/search/${searchTerm}?page=${page}`
      );
      const responseBody = await response.json();
      const movies = responseBody.Search;
      const totalResults = parseInt(responseBody.totalResults);
      setIsLoading(false);

      if (searchTerm === lastSearchTerm) {
        setMovies(prevMovies => [...prevMovies, ...movies]);
      } else {
        setMovies([...movies]);
        setLastSearchTerm(searchTerm);
      }

      if (totalResults - (page * 10) > 0) {
        setCanLoadMore(true);
        setPage(prevPage => prevPage + 1)
      } else {
        setCanLoadMore(false);
        setPage(1)
      }

      console.log('response', responseBody);
    } catch (error) {
      console.log(error);
      setIsLoading(false);
    }
  };

  useEffect(() => {
    if (searchTerm)
      fetchMovies();
  }, [searchTerm]);

  return [
    movies,
    setSearchTerm,
    isLoading,
    canLoadMore,
    fetchMovies,
    lastSearchTerm,
    setMovies,
  ];
}

export default useMovies;
```
Let's go over the code piece by piece.
```js
import { useState, useEffect } from 'react';
```
We begin by importing `useState` and `useEffect` from `react`. `React` doesn't need to be imported if we won't use any `JSX` in our hook. And yes you can return `JSX` in your hooks if you wish because they are React components.

```js
const [movies, setMovies] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [canLoadMore, setCanLoadMore] = useState(false);
  const [page, setPage] = useState(1)
  const [searchTerm, setSearchTerm] = useState(undefined)
  const [lastSearchTerm, setLastSearchTerm] = useState('')
```
Next inside the function we initialize some states that I won't go over again, because I've already discussed their use above.

```js
const fetchMovies = async () => {
    setIsLoading(true);
    if (searchTerm !== lastSearchTerm) {
      setPage(1);
      setMovies([]);
    }

    try {
      const response = await fetch(
        `https://movie-api-app.jgb.solutions/search/${searchTerm}?page=${page}`
      );
      const responseBody = await response.json();
      const movies = responseBody.Search;
      const totalResults = parseInt(responseBody.totalResults);
      setIsLoading(false);

      if (searchTerm === lastSearchTerm) {
        setMovies(prevMovies => [...prevMovies, ...movies]);
      } else {
        setMovies([...movies]);
        setLastSearchTerm(searchTerm);
      }

      if (totalResults - (page * 10) > 0) {
        setCanLoadMore(true);
        setPage(prevPage => prevPage + 1)
      } else {
        setCanLoadMore(false);
        setPage(1)
      }

      console.log('response', responseBody);
    } catch (error) {
      console.log(error);
      setIsLoading(false);
    }
  };
```
The fetchMovies is an async method (because we want to use async/await) that sets the loading state, set the pagination depending on whether we are searching for a new movie (series, tv show), that way we can fetch new stuff when needed. Next we use [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) to hit our API endpoint, extract the movies and totalResults from the response, set the loading state, appending the movies in our movies array or set the array to the movies, and update the `lastSearchTerm`. Then we check to see if we have more items to load for this term by subtracting the product of the number of pages we are in by 10, because 10 is the number of items we have per response.

Now we need to update the `App.js` file to import the `MovieList` component like so:
```js
import React from 'react';

import MovieList from './MovieList';

import './App.css';

function App() {
  return (
    <div className="container">
      <div className="row">
          <MovieList />
      </div>
    </div>
  );
}

export default App;
```
And with that our app should be able to display results for any query like so:
![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/fqhhsxynqag05xshvjpb.png)

Our `Load More` button can be clicked on to load more items for the same search:
![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/87cgcll2sut43ed2yjcw.png)

Note that we are making use of the API that I have setup so you need to setup your own for your app.

Cloudflare Workers is built on top of the [Service Worker](https://developers.google.com/web/fundamentals/primers/service-workers/) [API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) which is a somewhat new standard in browsers that allows you to do fancy stuff such as caching of assets, push notifications and more. It's a key feature that Progressive Web App makes use of. Cloudflare Workers uses the same V8 engine that Node.js and Google Chrome run on.

Now to the Cloudflare Workers API.
Use the [API](https://github.com/jgb-solutions/movie-app-workers-api) `starter` branch to have a head start.
Open the project in your code editor. We need to edit 2 files: `wrangler.toml` and `index.js`.
Head over to Cloudflare, creat an account if you haven't already and start adding a domain if have any. But one is not required to start using Cloudflare Workers. The account id and the zone id are required if you want to publish your worker to your own domain. You can create your own `wokers.dev` subdomain [here](https://workers.cloudflare.com/). You will also need your API key and your email. Once you have those last two, run `wrangler config` to configure your account with the CLI tool. You can also use environment variables every time you are publishing a worker like so:
```
CF_API_KEY=superlongapikey CF_EMAIL=testuser@example.com wrangler publish
```
Now open your `index.js` file and paste this bit of code:
```js
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event))
})

const API_KEY = `yourApiKey`
const API_URL = `http://www.omdbapi.com`

// if you want to fetch a single movie.
// const getSingleMovieUrl = movieId =>
//   `http://www.omdbapi.com/?i=${movieId}&apiKey=${API_KEY}`

const getSearchUrl = (searchTerm, page = 1) =>
  `http://www.omdbapi.com/?s=${searchTerm}&page=${page}&apiKey=${API_KEY}`

async function fetchApi(event) {
  const url = new URL(event.request.url)
  const uri = url.pathname.split('/')
  const page = url.searchParams.get('page')
  let urlToFetch = `https://movie-app-workers.jgb.solutions/`

  // if you want to fetch a single movie.
  // if (uri[1] === `movie`) urlToFetch = getSingleMovieUrl(uri[2])
  if (uri[1] === `search`) urlToFetch = getSearchUrl(uri[2], page)

  const cache = caches.default
  let response = await cache.match(event.request)

  if (!response) {
    response = await fetch(urlToFetch, { cf: { cacheEverything: true } })
    // const headers = { 'cache-control': 'public, max-age=31536000' }
    // response = new Response(response.body, { ...response, headers })
    event.waitUntil(cache.put(event.request, response.clone()))
  }
  return response
}

async function handleRequest(event) {
  if (event.request.method === 'GET') {
    let response = await fetchApi(event)
    if (response.status > 399) {
      response = new Response(response.statusText, { status: response.status })
    }
    return response
  } else {
    return new Response('Method not allowed', { status: 405 })
  }
}
```
We start by listening to the fetch event and then respond with a method that handle the request.
We set our API key that we get from [http://www.omdbapi.com/apikey.aspx](http://www.omdbapi.com/apikey.aspx), and the API url.

We then check to see whether the method of the request is `GET` otherwise we will just deny access. If they are requesting using `GET` then we use our helper function `fetchApi` that uses the event param to extract the path, the search term and the page query string. Once we have the new url we check in our cache whether we have a match. If we don't we fetch the url from the OMDb API and store the response in a `response` variable. What's interesting here is the second parameter where we pass `{ cf: { cacheEverything: true } }` to fetch, this is one way to tell Cloudflare to catch the response for as long as possible in its large network of data centers (they even have one in Port-au-Prince. Yay!). And then we return the response.

Now to test live we can run `wrangler preview` and it will build and publish our worker on Cloudflare and open a new browser tab for us to try our worker. And with that we are done with our worker function. I would advice using a tool such as Postman to test the API responses. One thing to pay attention to is the response header of the API. If Cloudflare cached the response it will send a header called `cf-cache-status` with a value of `HIT`, otherwise it will be equal to `MISS`. If you hit the API with the same term it should return `HIT` on the second request. If not you have done something wrong.

Don't forget to update your API url in the React app to use your own API key. :)

And with all that you have a very fast app that uses React, Hooks and Cloudflare Workers.

I hope that even if this tutorial was a bit long, you have learn a thing or two in it.

Do you have any suggestions or know or have built some more cool stuff with either of those technologies, just let me know in the comments. Thanks!

### Update
Hey there! If you need to host your websites or apps and you are on a budget then Vultr is a great place to start. You can try it for free and receive $100 in credits. I'll also receive $25 in credits if you do. [Click here](https://www.vultr.com/?ref=8402469-6G) to get your credits. Vultr is fast, reliable and cheap. [Get your $100 credits here](https://www.vultr.com/?ref=8402469-6G)