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 anN X N
grid based on the width of its children. The only thing you need to pass in is an array ofitems
. And here it is your first reusable component. You can use thisGrid
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 theGrid
. The code for theGridITem
component is below. Create a directory called GridItem and add in theindex.js
andstyled.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.js
and 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
withhttps://api.themoviedb.org/3
,type
withnow_playing
andConstants.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 usinguseState
anduseEffect
. Let's see what they do. - First
useState
. We are all familiar with defining astate object
in the constructor of a class component. Synonymous to that in a functional component we can define stateful variables using theuseState
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 theuseState
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 theset
function that was returned byuseState
the component is rerendered. eg. callingsetA()
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 theuseEffect
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 thereturn
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 theMain
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 drop a comment if you are stuck somewhere or would like to discuss something or give me some feedback.
Top comments (0)