React Hooks and OMDB API example

React Hooks + OMDB API

Today we will create a simple react app with the help of OMDB API requests. This app should compatible with search movies, series, and episodes by using the OMDB API and the detail feature by clicking on a particular movie/series. First, we need to obtain the API Key to use the OMDB request endpoints on their website which is completely free but with certain limits.

We will use the Ant Design to quickly set up the user interface section. It has many built-in components that save a lot of time. Let’s start the coding part without any further delay.

npx create-react-app omdb-app

The next step is to add the user interface dependencies, the Ant Design. Move to the working directory with cd omdb-app and install the dependencies.

npm i antd

We are going to use the Layout, Input, Row, Col etc. component to create the UI as displayed in the screenshot. Let’s open the App.js file and start adding the dependencies.

import React, { useEffect, useState } from 'react';
import { 
    Layout, 
    Input, 
    Row, 
    Col, 
    Card, 
    Tag, 
    Spin, 
    Alert, 
    Modal, 
    Typography 
} from 'antd';
import 'antd/dist/antd.css';

const API_KEY = 'cexxxxxxx6';
const { Header, Content, Footer } = Layout;
const { Search } = Input;
const { Meta } = Card;
const TextTitle = Typography.Title;

We need to implement 3 sections. First for Search, Second for Content that holds the cards for movies/series image and name, And the Third is the Modal that displays the ratings, detail, runtime, genre etc.

The next step is to create a basic layout that holds the Header, Content and the Footer section.

function App() {
    return (
        <div className="App">
            <Layout className="layout">
                <Header>
                    <div style={{ textAlign: 'center'}}>
                        <TextTitle style={{color: '#ffffff', marginTop: '14px'}} level={3}>OMDB API + React</TextTitle>
                    </div>
                </Header>
                <Content style={{ padding: '0 50px' }}>
                    <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
                        ...
                        ...
                        ...
                        ...
                    </div>
                </Content>
                <Footer style={{ textAlign: 'center' }}>OMDB Movies ©2019</Footer>
            </Layout>
        </div>
    );
}

export default App;

Search Container

The next step is to create the SearchBox component that holds the UI and Event handling part.

const SearchBox = () => {
    return (
        <Row>
            <Col span={12} offset={6}>
                <Search
                    placeholder="enter movie, series, episode name"
                    enterButton="Search"
                    size="large"
                    onSearch={value => console.log(value)}
                />
            </Col>
        </Row>
    )
}

Don’t get confused with the <Search /> component, It is borrowed from the Ant Design. The Search component is a part of the Input Component that we already imported at the top of our App.js file const { Search } = Input;

Content Container

The next step is to create a component that holds the Image and Title of the movie/series.

const ColCardBox = () => {

    return (
        <Col style={{margin: '20px 0'}} className="gutter-row" span={4}>
            <div className="gutter-box">
                <Card
                    style={{ width: 200 }}
                    cover={
                        <img
                            alt=""
                            src="https://placehold.it/198x264&text=Image+Not+Found"
                        />
                    }
                >
                    <Meta
                            title="Title Goes Here"
                            description={false}
                    />
                    <Row style={{marginTop: '10px'}} className="gutter-row">
                        <Col>
                            <Tag color="magenta">Movie / Series</Tag>
                        </Col>
                    </Row>
                </Card>
            </div>
        </Col>
    )
}

All this is just a user interface part, we have used the Row, Col, Card, Meta components by Ant Design.

The next step is to fit both the SearchBox and the ColCardBox component to our app.

function App() {
    return (
        <div className="App">
            <Layout className="layout">
                <Header>
                    ....
                </Header>
                <Content style={{ padding: '0 50px' }}>
                    <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>

                        <SearchBox />
                        <br />
                        <Row gutter={16} type="flex" justify="center">
                            <ColCardBox />
                        </Row>
                        
                    </div>
                </Content>
                <Footer style={{ textAlign: 'center' }}>OMDB Movies ©2019</Footer>
            </Layout>
        </div>
    );
}

export default App;

The next step is to call the OMDB API and display some default results. We are going to use the Effect and State Hook to achieve that.

Tip

If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMountcomponentDidUpdate, and componentWillUnmount combined.

Let’s define 4 states for data, error, loading and q. The data state should null by default and will hold the response object later. The error state should also be null and will update once we get the error. The loading state to display the spinner and the q state will hold the searched query parameter. We have set the batman as a default search parameter.

const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [q, setQuery] = useState('batman');

The next step is to call the OMDB API with the help of the Effect hook or we can call it the side effect.

useEffect(() => {

    setLoading(true);
    setError(null);
    setData(null);

    fetch(`http://www.omdbapi.com/?s=${q}&apikey=${API_KEY}`)
    .then(resp => resp)
    .then(resp => resp.json())
    .then(response => {
        if (response.Response === 'False') {
            setError(response.Error);
        }
        else {
            setData(response.Search);
        }

        setLoading(false);
    })
    .catch(({message}) => {
        setError(message);
        setLoading(false);
    })
}, [q]);

As you can see, we have injected the defined q state with the useEffect hook and used the q state with OMDB API. Let’s implement it with the user interface.

function App() {

    const [data, setData] = useState(null);
    const [error, setError] = useState(null);
    const [loading, setLoading] = useState(false);
    const [q, setQuery] = useState('batman');


    useEffect(() => {

        setLoading(true);
        setError(null);
        setData(null);

        fetch(`http://www.omdbapi.com/?s=${q}&apikey=${API_KEY}`)
        .then(resp => resp)
        .then(resp => resp.json())
        .then(response => {
            if (response.Response === 'False') {
                setError(response.Error);
            }
            else {
                setData(response.Search);
            }

            setLoading(false);
        })
        .catch(({message}) => {
            setError(message);
            setLoading(false);
        })

    }, [q]);

    return (
        <div className="App">
            <Layout className="layout">
                <Header>
                    ....
                </Header>
                <Content style={{ padding: '0 50px' }}>
                    <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
                        <SearchBox />
                        <br />
                        <Row gutter={16} type="flex" justify="center">
                            { loading &&
                                <Loader />
                            }

                            { error !== null &&
                                <div style={{margin: '20px 0'}}>
                                    <Alert message={error} type="error" />
                                </div>
                            }
                            
                            { data !== null && data.length > 0 && data.map((result, index) => (
                                <ColCardBox 
                                    {...result} 
                                />
                            ))}
                        </Row>
                    </div>
                </Content>
                <Footer style={{ textAlign: 'center' }}>OMDB Movies ©2019</Footer>
            </Layout>
        </div>
    );
}

Now we have to modify our ColCardBox component to display the data like Title, Poster and Type.

const ColCardBox = ({Title, Poster, Type}) => {

    return (
        <Col style={{margin: '20px 0'}} className="gutter-row" span={4}>
            <div className="gutter-box">
                <Card
                    style={{ width: 200 }}
                    cover={
                        <img
                            alt={Title}
                            src={Poster === 'N/A' ? 'https://placehold.it/198x264&text=Image+Not+Found' : Poster}
                        />
                    }
                >
                    <Meta
                            title={Title}
                            description={false}
                    />
                    <Row style={{marginTop: '10px'}} className="gutter-row">
                        <Col>
                            <Tag color="magenta">{Type}</Tag>
                        </Col>
                    </Row>
                </Card>
            </div>
        </Col>
    )
}

Now you will get the result that matches the string batman. Because batman is the default search parameter. The next step is to attach the SearchBox event. We only update the search query parameter because of the useEffect which is identical to componentDidUpdate. We have to update our SearchBox component slightly to make it work.

const SearchBox = ({searchHandler}) => {
    return (
        <Row>
            <Col span={12} offset={6}>
                <Search
                    placeholder="enter movie, series, episode name"
                    enterButton="Search"
                    size="large"
                    onSearch={value => searchHandler(value)}
                />
            </Col>
        </Row>
    )
}


<SearchBox searchHandler={setQuery} />

As you can see, we have attached the setQuery event with searchHandler property in SearchBox component.

Now if you try to search for something and hit enter or press the Search Button, then it will return the updated result.

Implement Modal Box

The next step is to implement the Modal box when we click on any of the Card. It is very easy to handle by adding three more states.

const [activateModal, setActivateModal] = useState(false);
const [detail, setShowDetail] = useState(false);
const [detailRequest, setDetailRequest] = useState(false);

The activateModal state helps to close the Modal Component. The detail state is used to collect the data. and the detailRequest state is used to display the loader.

First, we will attach the Modal component and later will create a MovieDetail component that displays the returned details.

<Content style={{ padding: '0 50px' }}>
    <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
        <SearchBox searchHandler={setQuery} />
        <br />
        
        <Row gutter={16} type="flex" justify="center">
            ...
            ...
            ...
        </Row>
    </div>
    <Modal
        title='Detail'
        centered
        visible={activateModal}
        onCancel={() => setActivateModal(false)}
        footer={null}
        width={800}
        >
        { detailRequest === false ?
            (<MovieDetail {...detail} />) :
            (<Loader />) 
        }
    </Modal>
</Content>

Now let’s create the MovieDetail component.

const MovieDetail = ({Title, Poster, imdbRating, Rated, Runtime, Genre, Plot}) => {
    return (
        <Row>
            <Col span={11}>
                <img 
                    src={Poster === 'N/A' ? 'https://placehold.it/198x264&text=Image+Not+Found' : Poster} 
                    alt={Title} 
                />
            </Col>
            <Col span={13}>
                <Row>
                    <Col span={21}>
                        <TextTitle level={4}>{Title}</TextTitle></Col>
                    <Col span={3} style={{textAlign:'right'}}>
                        <TextTitle level={4}><span style={{color: '#41A8F8'}}>{imdbRating}</span></TextTitle>
                    </Col>
                </Row>
                <Row style={{marginBottom: '20px'}}>
                    <Col>
                        <Tag>{Rated}</Tag> 
                        <Tag>{Runtime}</Tag> 
                        <Tag>{Genre}</Tag>
                    </Col>
                </Row>
                <Row>
                    <Col>{Plot}</Col>
                </Row>
            </Col>
        </Row>
    )
}

Now we have to implement the click event to the card and another request to collect that particular movie/series data. The next step is to modify the ColCardBox component.

First, implement the newly defined state handler to the ColCardBox.

{ data !== null && data.length > 0 && data.map((result, index) => (
    <ColCardBox 
        ShowDetail={setShowDetail} 
        DetailRequest={setDetailRequest}
        ActivateModal={setActivateModal}
        key={index} 
        {...result} 
    />
))}

Now modify the ColCardBox component.

const ColCardBox = ({Title, imdbID, Poster, Type, ShowDetail, DetailRequest, ActivateModal}) => {

    const clickHandler = () => {

        // Display Modal and Loading Icon
        ActivateModal(true);
        DetailRequest(true);

        fetch(`http://www.omdbapi.com/?i=${imdbID}&apikey=${API_KEY}`)
        .then(resp => resp)
        .then(resp => resp.json())
        .then(response => {
            DetailRequest(false);
            ShowDetail(response);
        })
        .catch(({message}) => {
            DetailRequest(false);
        })
    }

    return (
        <Col style={{margin: '20px 0'}} className="gutter-row" span={4}>
            <div className="gutter-box">
                <Card
                    style={{ width: 200 }}
                    cover={
                        <img
                            alt={Title}
                            src={Poster === 'N/A' ? 'https://placehold.it/198x264&text=Image+Not+Found' : Poster}
                        />
                    }
                    onClick={() => clickHandler()}
                >
                    <Meta
                            title={Title}
                            description={false}
                    />
                    <Row style={{marginTop: '10px'}} className="gutter-row">
                        <Col>
                            <Tag color="magenta">{Type}</Tag>
                        </Col>
                    </Row>
                </Card>
            </div>
        </Col>
    )
}

We have added the onClick={() => clickHandler()} on the Card component. The clickHandler event will take send a request and grab the detail of a particular movie/series.

If you are facing any issue to use this example, please don’t hesitate to contact me through the comment section.