Build a cool movie database using React Hooks
Tags: tutorial, webdev, react, javascript
Published: 2020-06-16 12:41:32

# Introdution
In this article I will show you the project I built to learn some React Hooks with unit tests. The application is a simple page that fetches a movie list using HTTP request and display the result. This article will also cover unit tests using [react testing library](https://testing-library.com/docs/react-testing-library/intro).

Demo on CodeSandbox:
{% codesandbox gm87l %}

Final code is on github:
{% github fabiothiroki/react-hooks-movies no-readme %}

Thanks for [Reiha Hosseini](https://codepen.io/mrReiha/pen/RwPgLeM) for the frontend design development.

# Setup
As a prerequisite you must have [Node >= 8.10 and npm >= 5.6](https://nodejs.org/) installed on your computer.

First we will create the application structure using [create-react-app](https://github.com/facebook/create-react-app):
```bash
npx create-react-app react-hooks-movies
cd react-hooks-movies
npm start
```

At this step, my project was built using React version 16.13.1.

# Initial project structure
The `creact-react-app` creates a basic `App` component in the project root directory. We will move this file and its related to its own folder component to keep things more organized. I personally prefer to create a `components` folder and move all `App` components files to its own `App` component folder. Then you just need to change the `App.js` import path on `index.js`:
```
import App from './components/App/App';
```

![Project folder structure](https://dev-to-uploads.s3.amazonaws.com/i/8t2z6q29b3mo2sos4h5h.png)

Check your application on [http://localhost:3000/](http://localhost:3000/) and everything should be working as before.

Optionally you can copy my [index.css](https://github.com/fabiothiroki/react-hooks-movies/blob/master/src/index.css) content so you have the same result as me.

# Api
Now we can start writing the additional modules needed. We will start by writing the one responsible for making the http request to retrieve the movie data. We could build this part directly on `App` component but creating a separate module for this will help us write the tests in the near future.

The API used is the free [OMDB](https://www.omdbapi.com/). Be sure to signup there to get your own [API Key](https://www.omdbapi.com/apikey.aspx), and place it as an environment variable called `REACT_APP_API_KEY`. This way you won't expose your key if you want to share your code on Github.

To get some fancy results and cool posters, we will use the `search` parameter to fetch movies that have 'war' on its name:

```javascript
// src/api/api.js
const MOVIE_API_URL = `https://www.omdbapi.com/?apikey=${process.env.REACT_APP_API_KEY}`;

export const fetchMovies = (search = 'war') => (
  fetch(`${MOVIE_API_URL}&s=${search}`)
  .then(response => response.json())
);
```
As you can see we're already returning a `Promise` containing a parsed JSON.

# Movie component
This component will render each movie data returned in the array of the previous module. No secret here, just a plain `React` component:
```javascript
// src/components/Movie/Movie.js
import React from "react";

const Movie = ({ movie }) => {
  return (
    <figure className="card">
      <img 
        src={movie.Poster}
        alt={`The movie titled: ${movie.Title}`}
      />
      <figcaption>{movie.Title}</figcaption>
    </figure> 
  );
};

export default Movie;
```

# Reducer
The `reducer` is a `function` that receives an `action` object, and a `state` object and returns the new `state` that will be rendered by `App` component. So basically we will use the `reducer` function to handle the application state, that will be managed by three variables: `loading`, `movies` and `error`.

```javascript
// src/components/App/reducer.js
export const initialState = {
  loading: true,
  movies: [],
  errorMessage: null
};
```

In this case I prefer the `useReducer` hook instead of the `useState` hook because I this state is complex enough.

The only action we need for now is the one dispatched when the API request returns successfully, we will call it `SEARCH_MOVIES_SUCCESS`.

```javascript
export const reducer = (state, action) => {
  switch (action.type) {
    case "SEARCH_MOVIES_SUCCESS":
      return {
        loading: false,
        movies: action.payload,
        errorMessage: null,
      };
    default:
      return state;
  }
};
```

So whenever we receive this action, we update the current state to hide both the `loading` indicator and the `error` message and we update with API Response.

# App component
Now on `App` component we just need to glue the `api` with its `reducer` and the `Movie` component.

## UseReducer hook
The `useReducer` hook is a function that receives a `reducer` function like the one we have implemented on previous step and an object representing the initial state as second argument. Its return are two variables, the current state and a method for dispatching actions.

So first we add all new `imports`:
```javascript
// src/components/App/App.js
import React, { useReducer } from 'react';
import { initialState, reducer } from "./reducer";
import Movie from "../Movie/Movie";
``` 

Now we can call `useReducer` inside the functional component, get the initial state from it and render.
```javascript
export const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  const { movies, errorMessage, loading } = state;

  return (
    <div className="wrapper">
      <h2><strong>Movies</strong></h2>
      <div className="cards">

      {loading &&
        <span>loading...</span>
      }

      {errorMessage &&
        <span>{errorMessage}</span>
      }

      {movies &&
        movies.map((movie, index) => (
          <Movie key={`${index}-${movie.Title}`} movie={movie} />
        ))
      }

      </div>
    </div>
  );
};
```

If you run the current application on browser, you can notice the correct rendering of `loading` state.

## UseEffect hook
Finally we will effectively render the cool movie posters. But what does this hook do?
> By using this Hook, you tell React that your component needs to do something after render.

So in this case we will do the movies data fetching.

First, start adding the new imports:
```javascript
import React, { useEffect, useReducer } from 'react';
import { fetchMovies } from '../../api/api'; 
```

Then inside the component, just after defining the `dispatch` method you can already call the `hook`:
```javascript
export const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    fetchMovies()
      .then(jsonResponse => {
        dispatch({
          type: "SEARCH_MOVIES_SUCCESS",
          payload: jsonResponse.Search
        });
      });
  }, []);
  
  // Hidden previous code
}
```

The first parameter of `useEffect` is a function containing the effect itself and the second parameter is an array of values that the effect depends on, in case we want to conditionally fire this effect. In this case we can pass an empty array because this is a static page.

# DOM Testing
At this step your application is working and hopefully is looking the same as the demo.

So now we will focus on testing with tools:
- [Jest 24.9.0](https://jestjs.io/): the test runner.
- [React Testing Library 9.5.0](https://testing-library.com/docs/react-testing-library/intro): testing utility that encourages the developer to write tests resembling the way the user sees the application.

The first prerequisite for this test is the `mock` of the API part, because we're considering this is an external dependency and in this particular case we don't want to write automated tests. I've just copied the same `json` response of the API but kept only one movie to make the test simple and clean.

```javascript
// src/components/App/App.test.js
import { fetchMovies } from '../../api/api'; 

jest.mock('../../api/api');

const mockReponse = {
  "Search": [{
    "Title": "Avengers: Infinity War", 
    "Year": "2018", 
    "imdbID": "tt4154756", 
    "Type": "movie", 
    "Poster": "https://m.media-amazon.com/images/M/MV5BMjMxNjY2MDU1OV5BMl5BanBnXkFtZTgwNzY1MTUwNTM@._V1_SX300.jpg" 
  }],
  "totalResults": "3964",
  "Response": "True"
};

beforeEach(() => {
  fetchMovies.mockResolvedValueOnce(Promise.resolve(mockReponse));
});
```

By using Jest Mock feature, we're basically telling the tests to return the `mockResponse` whenever `fetchMovies` is called.

The test case we will focus on will consist by the following assertions in the following order:
1. After `App` component is renderd, it should display the `loading` state.
2. Then it should trigger the API request, and if successful, the `loading` state should be hidden.
3. The movies should be rendered.

```javascript
// src/components/App/App.test.js
import React from 'react';
import { render, waitForElementToBeRemoved } from '@testing-library/react';
import App from './App';

// mock configuration...

test('renders loading first, then movies', async () => {
  const { getByText } = render(<App />);
  
  // Should display loading state
  expect(getByText(/loading/i)).toBeTruthy();

  // Should trigger API request
  expect(fetchMovies).toHaveBeenCalledTimes(1);

  // Should hide loading state
  await waitForElementToBeRemoved(() => getByText(/loading/i));

  // Should display the movie returned on mock
  expect(getByText(/Avengers/i)).toBeTruthy();
});
```

As an exercise for the reader, you could also write the test when the API returns an error.

Now if you run `npm run test`, this test should pass sucessfully!

# Conclusion
I hope you could've learn the basics on how to consume an API and render its data using React Hooks! Automated tests should also be part of your application so I hope you enjoyed this part as well.