Creating a searchable movie app using React, styled-components and react hooks
Tags: react, hooks
Published: 2019-11-14 21:11:47

With React Hooks becoming the new normal, they have certainly got me more hooked on to React. The other day I was commenting on a post [here](https://dev.to/manan30/comment/hk8a) and figured I should write this article. In this article, we will be creating a small searchable movie database using the [TMDb API](https://www.themoviedb.org/) completely using React hooks. We will also see how we can create 100% reusable components. So without any further delay, let's get started.

## Project Setup

Let's create a new react app. The best way to do this is by using `npx create-react-app movie-database`. Next, we want to install `styled-components` for styling our app and `axios` to make network requests. Install them by using `npm i axios styled-components` or `yarn add axios styled-components`.

With our project dependencies installed, let's generate a key [here](https://developers.themoviedb.org/3/getting-started/introduction) to access the TMDb API. Since the link at the top has detailed steps I am not going to go through the steps here. But, if you feel stuck at any of the steps, feel free to drop a comment below.

I hope you were successfully able to generate a key! Please copy the key and paste it somewhere we will need that key in some time.

## Overview

Now with our project all set up. Let's understand how things are going to work and what kind of hooks we will use. First up, some basic intro to hooks. Traditionally we have thought of functional components to be dumb components that don't have their state and life-cycle methods. Hence, this did not allow us to make efficient reusable components and class components, on the other hand, had a lot of boiler-plate associated with them even to perform a simple operation. But, hooks change our way of thinking completely. With hooks, we can make any functional component stateful and even perform life-cycle operations inside it. In this article we will be looking at two React hooks namely `useState` and `useEffect`. The `useState` hook allows us to add state variables to our functional components while `useEffect` helps in achieving the tasks that we normally do in life-cycle methods. React also allows us to define our custom hooks but more on this later. Read more about React hooks [here](https://reactjs.org/docs/hooks-overview.html).

---

Also, we will be using [styled-components](https://www.styled-components.com/) for styling the app but you can use CSS or any other preprocessor.

So, let's start creating a few components. First up we are going to create a grid component that is going to display all the movies. Create a directory called Grid and add in the `index.js` and `styled.js` files.

### Grid Component

#### Grid/index.js

```react
import React from 'react';
import PropTypes from 'prop-types';

import GridItem from '../Item';

import GridContainer from './styled';

import Constants from '../../utils/Constants';

function Grid({ items }) {
  return (
    <GridContainer>
      {items.map((item, i) => {
        const idx = i;
        return (
          <GridItem
            key={idx}
            title={item.title}
            image={`${Constants.IMAGE_URL}/${item.poster_path}`}
            overview={item.overview}
            ratings={item.vote_average}
          />
        );
      })}
    </GridContainer>
  );
}

Grid.propTypes = {
  items: PropTypes.arrayOf(PropTypes.any)
};

Grid.defaultProps = {
  items: []
};

export default Grid;
```

#### Grid/styled.js

```react
import styled from 'styled-components';

const GridContainer = styled.div`
  display: flex;
  width: 100%;
  flex-direction: row;
  flex-wrap: wrap;
`;

export default GridContainer;
```

- Let's see how this component works. The `Grid` component will create an `N X N` grid based on the width of its children. The only thing you need to pass in is an array of `items`. And here it is your first reusable component. You can use this `Grid` in any project. You can pass in props or use any other component as its children.
- In this example, I have created a `GridItem` component as the child for the `Grid`. The code for the `GridITem` component is below. Create a directory called GridItem and add in the `index.js` and `styled.js` files.

### GridItem component

#### GridItem/index.js

```react
import React from 'react';
import PropTypes from 'prop-types';

import {
  Container,
  Content,
  Image,
  Text,
  FAB,
  Separator,
  Button
} from './styled';

function Item({ image, title, overview, ratings }) {
  return (
    <Container>
      <Image image={image} />
      <Content>
        <Text weight='bolder' relative>
          {title}
        </Text>
        <Text color='#BFC0CE' height>
          {overview}
        </Text>
        <FAB>{ratings}</FAB>
        <Separator />
        <Button>Details</Button>
      </Content>
    </Container>
  );
}

Item.propTypes = {
  image: PropTypes.string,
  title: PropTypes.string,
  overview: PropTypes.string,
  ratings: PropTypes.string
};

Item.defaultProps = {
  image: '',
  title: '',
  overview: '',
  ratings: ''
};

export default Item;
```

#### GridItem/styled.js

```react
import styled from 'styled-components';

const Container = styled.div`
  display: inline-flex;
  height: 150px;
  width: calc(50% - 45px);
  margin-top: 16px;
  margin-bottom: 20px;
  margin-right: 15px;
  padding: 15px;
  background: white;
  box-shadow: 10px 5px 15px #e0e5ec;
`;

const Image = styled.div`
  height: 128px;
  width: 90px;
  margin-top: -32px;
  background-color: white;
  background-image: url(${props => props.image && props.image});
  background-position: center center;
  background-repeat: no-repeat;
  background-size: cover;
  box-shadow: 3px 2px 4px #dbdee3;
`;

const Content = styled.div`
  height: 100%;
  width: 100%;
  margin-left: 20px;
  margin-top: 5px;
  margin-bottom: 15px;
`;

const Text = styled.div`
  position: relative;
  margin-bottom: 15px;
  height: ${props => props.height && '3.6em'};
  font-size: ${props => (props.size && props.size) || '16px'};
  font-weight: ${props => (props.weight && props.weight) || ''};
  color: ${props => (props.color && props.color) || '#9D9FB0'};
  overflow: hidden;
  ::after {
    content: '';
    text-align: right;
    position: absolute;
    bottom: 0;
    right: 0;
    width: ${props => (props.relative && '0') || '40%'};
    height: 1.2em;
    background: linear-gradient(
      to right,
      rgba(255, 255, 255, 0),
      rgba(255, 255, 255, 1) 50%
    );
  }
`;

const FAB = styled.div`
  display: flex;
  height: 48px;
  width: 48px;
  margin-top: -150px;
  border-radius: 50%;
  float: right;
  color: white;
  box-shadow: 4px 4px 10px #c9d8db;
  background-color: #2879ff;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: bold;
`;

const Separator = styled.hr`
  position: relative;
  height: 2px;
  margin: 10px 0;
  background: #f2f4f8;
  border: none;
`;

const Button = styled.div`
  display: flex;
  width: 64px;
  padding: 5px;
  margin-right: 5px;
  float: right;
  justify-content: center;
  align-items: center;
  font-size: 12px;
  border-radius: 10px;
  border: 2px solid #2879ff;
  color: #2879ff;
  cursor: pointer;
  :hover {
    background: #2879ff;
    color: white;
    box-shadow: 2px 0 7px #c9d8db;
  }
`;

export { Container, Content, Image, Text, FAB, Separator, Button };
```

---

With our Grid component in place, let's fetch some data to display. We'll be using axios to fetch data from the TMDb API. Time to bring out the API key that we had created earlier.

Let's create a file called `API.js`and use the code below.

### API.js

```js
import axios from 'axios';

const movies = type => {
  return axios.get(
    `${Constants.REQUEST_URL}/movie/${type}?api_key=${Constants.API_KEY}`
  );
};

export default { movies };
```

- Replace `Constants.REQUEST_URL` with `https://api.themoviedb.org/3`, `type` with `now_playing` and `Constants.API_KEY` with `<the_api_key_you_created_earlier>`.

---

Let's now tie everything together in our view and see hooks in action. Create a directory called `Main` and add the two files shown below. This is our main view and our movie grid will be shown here.

### Main View

#### Main/styled.js

```react
import styled from 'styled-components';

const RootContainer = styled.div`
  height: 100vh;
  width: 100vw;
  display: inline-flex;
`;
const SideBarSection = styled.section`
  width: 20%;
  background-color: white;
  box-shadow: 3px 0 15px #e5e9f0;
`;

const ContentSection = styled.div`
  height: 100%;
  width: 100%;
`;

const SearchBarSection = styled.section`
  height: 38px;
  width: 256px;
  margin: 10px 0;
  padding: 0 20px;
`;

const MoviesGridSection = styled.section`
  height: calc(100% - 88px);
  width: calc(100% - 28px);
  padding: 20px;
  overflow-y: scroll;
`;

export {
  RootContainer,
  SideBarSection,
  ContentSection,
  SearchBarSection,
  MoviesGridSection
};
```

#### Main/index.js

```react
import React, { useState, useEffect } from 'react';

import Search from '../../components/Search';
import MoviesGrid from '../../components/Grid';

import Get from '../../api/Get';
import Constants from '../../utils/Constants';
import useSearch from '../../hooks/useSearch';

import {
  RootContainer,
  ContentSection,
  MoviesGridSection,
  SearchBarSection
} from './styled';

Constants.FuseOptions.keys = ['title'];

function Main() {
  const [movies, setMovies] = useState({});
  const [movieType, setMovieType] = useState('');

  useEffect(() => {
    try {
      (async () => {
        const popularMovies = await Get.movies('now_playing');
        setMovies(state => {
          const newState = { ...state };
          newState.now_playing = popularMovies.data.results;
          return newState;
        });
        setMovieType('now_playing');
      })();
    } catch (e) {
      console.log({ e });
    }
  }, []);

  return (
    <RootContainer>
      <ContentSection>
        <MoviesGridSection>
          <MoviesGrid items={results} />
        </MoviesGridSection>
      </ContentSection>
    </RootContainer>
  );
}

export default Main;
```

- In the `index.js` file we are using `useState` and `useEffect`. Let's see what they do.
- First `useState`. We are all familiar with defining a `state object` in the constructor of a class component. Synonymous to that in a functional component we can define stateful variables using the `useState` hook.
- `useState` is nothing but a JavaScript function that takes in an initial value as an argument and returns us an array. eg. `const [A, setA] = useState(0)`. Here we are passing the `useState` hook an initial value of 0 and it returns to us an array with two entries. The first is the current value of that variable and the second one is a function to set that value.
- As a comparison, the state variable in the above code in a class component would look like this

```js
this.state = {
  movies: {},
  movieType: ''
};
```

- We know that whenever we do `this.setState()` in a class component it is rerendered. Similarly when we call the `set` function that was returned by `useState` the component is rerendered. eg. calling `setA()` in the above point would rerender the component.
- And this is `useState` in a nutshell. At the end of the day, it allows you to declare state variables.

---

- Moving on to `useEffect`. useEffect allows us to do the tasks that we used to do in the life-cycle methods.
- useEffect is much more involved than useState. It takes in as arguments a callback function and an optional dependency array. It looks like this `useEffect(callback, <dependencies>)`.
- The `callback` function specifies what the effect should do while the dependencies array tells when the effect needs to be run.
- If useEffect does not have a dependency array it will run on every render, if it is an empty array it will run only on the first render and if the dependency array has contents, it will run whenever those dependencies change.
- Specifying an empty array can be used to do tasks that we usually do in the `componentDidMount()` life-cycle method. Since we want to fetch the data only once we have used an empty array in the `useEffect` hook in the code.

---

Go ahead and run the app using `npm start` and you'll be able to see a list of movies.

Next, we want to add a search to our app.

---

- In this app, we will be using [Fuse.js](https://fusejs.io/) to perform a fuzzy search in our app.
- Go ahead and install the fuse.js module using `npm install fuse.js`.

First, let's add a Search component to the app. Create a directory called Search and add in the `index.js` and `styled.js` files.

### Search component

#### Search/index.js

```react
import React from 'react';
import { MdSearch } from 'react-icons/md';
import PropTypes from 'prop-types';

import { SearchBarContainer, SearchIcon, SearchInput } from './styled';

function Search({ handler, value }) {
  return (
    <SearchBarContainer>
      <SearchIcon>
        <MdSearch />
      </SearchIcon>
      <SearchInput
        onChange={handler}
        value={value}
        placeholder='Search Movies'
      />
    </SearchBarContainer>
  );
}

Search.propTypes = {
  handler: PropTypes.func,
  value: PropTypes.string
};

Search.defaultProps = {
  handler: () => {},
  value: ''
};

export default Search;
```

#### Search/styled.js

```react
import styled from 'styled-components';

const SearchBarContainer = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  width: 100%;
  border-bottom: 2px solid #dfe5ef;
`;

const SearchIcon = styled.div`
  display: inline-flex;
  height: 24px;
  width: 24px;
  color: #9d9fb0;
  font-size: 14px;
  font-weight: bolder;
  svg {
    height: 100%;
    width: 100%;
  }
`;

const SearchInput = styled.input`
  height: 24px;
  width: 100%;
  margin-left: 10px;
  border: none;
  background-color: transparent;
  color: #9d9fb0;
  font-size: 14px;
  font-weight: bolder;
`;

export { SearchBarContainer, SearchIcon, SearchInput };
```

- We are going to add this component to our `Main` view. Replace the contents of the `return` with the code below.

```react
return (
    <RootContainer>
      <ContentSection>
        <SearchBarSection>
          <Search handler={e => search(e.target.value)} value={searchTerm} />
        </SearchBarSection>
        <MoviesGridSection>
          <MoviesGrid items={results} />
        </MoviesGridSection>
      </ContentSection>
    </RootContainer>
  );
```

- Now we will be writing a custom hook that can perform the search for us.
- Create a new file called `useSearch.js` and add the code given below.

```js
import { useState } from 'react';
import Fuse from 'fuse.js';

function search({ fuse, data, term }) {
  const results = fuse.search(term);
  return term ? results : data;
}

function useSearch({ data = [], options }) {
  const [searchTerm, setSearchTerm] = useState('');
  const fuse = new Fuse(data, options);
  const results = search({ fuse, data, term: searchTerm });
  const reset = () => setSearchTerm('');
  return { results, search: setSearchTerm, searchTerm, reset };
}

export default useSearch;
```

- As you can see we are using the `useState` React hook to create a custom hook. This hook takes in the array that we want to search through and options to pass into fuse.js. For our app, we will be searching the movies list based on their names.
- Let's use this hook in our `Main` view.
- Copy the code below and paste it below `useEffect` in the `Main` view render function.

```js
const { results, search, searchTerm } = useSearch({
  data: movies[movieType],
  options: Constants.FuseOptions
});
```

- And there it is, we just added search to our app. You'll be able to search through the movies based on their titles.

---

As you can see React hooks make things so much cleaner and easier to understand. I love hooks and hope after this article you look hooks too.

As always feel free to drop a comment if you are stuck somewhere or would like to discuss something or give me some feedback.

Checkout the [demo](https://manan30.github.io/movies-db/) and complete [code](https://github.com/manan30/movies-db).

Find me on [Twitter](https://twitter.com/Manan_30) and [Instagram](https://www.instagram.com/manan__joshi/)