Creating a searchable movie app using React, styled-components and react hooks

Nov 14, 2019

9 min read

Last Updated On Sep 22, 2021

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 and figured I should write this article. In this article, we will be creating a small searchable movie database using the TMDb API 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 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.

- -

Also, we will be using styled-components 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

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

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

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

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.jsand use the code below.

API.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

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

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
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 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

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

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.
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.
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.
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 reach out if you are stuck somewhere or would like to discuss something or give me some feedback.

Checkout the demo and complete code.

Find me on Twitter and Instagram