loading...
Cover image for Creating a movie website with GraphQL and React - part two

Creating a movie website with GraphQL and React - part two

aurelkurtula profile image aurel kurtula Updated on ・9 min read

Creating a movie website with GraphQL and React (2 Part Series)

1) Creating a movie website with GraphQL and React - part one 2) Creating a movie website with GraphQL and React - part two

In part one we created the GraphQL API. Now we are going to create a react application that makes use of that API.

Before we move on, just because I thought it to be cool, we could use an HTTP client, like axios, to make requests to our GraphQL server! Check this out:

const query = `{
    newMovies {
      id
    title
    }  
}`
const url = 'http://localhost:4000/graphql?query='+query;

axios.get(url)
  .then(res => console.log(res.data.data.newMovies))

If you are interested, you can see that setup in action by paying attention to the url changes when using the graphQL interface - we worked on in part one

However, to make production easier and pleasant, instead of using an HTTP client there are GraphQL clients which we can use.

There are few clients to pick from. In this tutorial I am going to use the Apollo Client. Apollo provides a graphQL server as well but we already created that with express-graphql so we are not using that part of Apollo, but the Apollo Client, as the name suggests is the part which gives us the ability to write GraphQL in react.

In a nut shell

If you want to follow along you should clone the repository from github, checkout the branch name Graphql-api and since we're going to focus on react side now, all the code is going to be writen in the client directory, which is the react application code.

Clearly this is not a beginner's tutorial. If you don't know react but are interested in learning the basics I've writen an introduction to it.

First install the following packages.

npm install apollo-boost react-apollo graphql-tag graphql --save

The game plan is to wrap our react app with an ApolloProvider which in turn adds the GraphQL client into the react props. Then make graphQL queries through graphql-tag.

At the moment, in ./client/index.js you see this setup

import React from 'react';
import ReactDOM from 'react-dom';
import './style/style.scss';
const App = () => {
  return <div>Hello World2</div>
}
ReactDOM.render(
  <App />,
  document.querySelector('#root')
);

First step, wrap the entire app with the ApolloProvider. The provider also needs a GraphQL client to pass to react.

import { ApolloProvider, graphql } from 'react-apollo';
...
const client = new ApolloClient({
  uri: "http://localhost:4000/graphql"
});
ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider> ,
  document.querySelector('#root')
);

The ApolloClient requires a uri if the GraphQL server doesn't point at /graphql. So in our case leaving it out and just using new ApolloClient() would work

Now that we have access to the client we can make queries llike so:

import { ApolloProvider, graphql } from 'react-apollo';
import gql from 'graphql-tag';
import ApolloClient from 'apollo-boost';

const AppComponent = (props) => {
  if(props.data.loading) return '<div>loading</div>';
  return <div>{props.data.newMovies[0].title}</div>
}
const query = gql`{ newMovies { title } }`;
const App = graphql(query)(AppComponent)

We wrap the AppComponent with graphql, we also inject the query into the props so then props.data.newMovies gives us the movie results.

Let's get started

Because the application we are building is bigger then the above example of displaying a single title, lets split it out.

Start from ./client/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';
import { HashRouter, Switch, Route } from 'react-router-dom'
import NewMovies from './components/NewMovies';
import './style/style.scss';
const client = new ApolloClient();
const Root = () => {
  return (
    <HashRouter >
      <ApolloProvider client={client}>
      <Switch >
        <Route exact path="/" component={NewMovies} />
      </Switch>
      </ApolloProvider>
    </HashRouter>
  )
}
ReactDOM.render(
  <Root />,
  document.querySelector('#root')
);

Simple, a couple of routes. the imported component (NewMovies) don't exist yet but that's all the code required in ./client/index.js.

Again, all the components that we would ever use would be specified within the Switch component. Therefore the entire app is wrapped with in the ApolloProvider, exactly the same as in the nutshell section.

Getting top movies

Let's create a file at ./client/components/NewMovies.js, and start by importing the required packages

import React, { Component} from 'react'
import gql from 'graphql-tag'
import { graphql } from 'react-apollo'

Next, inject the newMovies GraphQL query results into the NewMovies Component

class NewMovies extends Component {
...
}
const query = gql`
{
    newMovies {
        id
        poster_path
        title
    }
}
`
export default graphql(query)(NewMovies);

With that setup, an object array gets injected into the NewMovies component props and can be accessed by this.props.data.newMovies. Let's make use of them:

class NewMovies extends Component {
    Movies(){
        return this.props.data.newMovies.map(movie => {
            return (
                <article key={movie.id} className="movie_list">
                    <img src={movie.poster_path} />
                    <h1>{movie.title}</h1>
                </article>
            );
        })
    }
    render() {
        if(this.props.data.loading) return <div>loading</div>
        return this.Movies()
    }
}

There we have it. Things to note are

  • The react component loads before the newMovies results are fetched.
  • graphql gives us a loading property which is set to true whilst data is fetched, and false when the data is ready to be used

Before we move on to another component, lets wrap the movie posters with an anchor so that we get more information when one poster is selected.

To do so we'll use the Link component from the react-router-dom package.

import { Link } from 'react-router-dom'

class NewMovies extends Component {
    Movies(){
        return this.props.data.newMovies.map(movie => {
            return (
                <article key={movie.id} className="movie_list">
                    <Link to={"/info/"+movie.id}> 
                        <img src={movie.poster_path} />
                    </Link>
    ...

Whenever a poster is clicked we are directed to /info/1 for example.

We need to head back to ./client/index.js and add a router which catches that route.

...
import MovieInfo from './components/MovieInfo';
...
const Root = () => {
  return (
    <HashRouter >
      <ApolloProvider client={client}>
      <Switch >
        <Route exact path="/" component={TopMovies} />
        <Route exact path="/info/:id" component={MovieInfo} />
      </Switch>
      ...

Of course, that's the power of react routing (covered here before).

Let's work on MovieInfo Component

Start by creating the file at ./client/components/MovieInfo.js then add the following:

import React, { Component } from 'react'
import gql from 'graphql-tag'
import { graphql } from 'react-apollo'
class MovieInfo extends Component {
    render(){
        if(this.props.data.loading) return <div>loading</div>
        return (
            <div>{this.props.data.movieInfo.title}</div>
        )
    }
}
const query = gql`
{movieInfo(id: "284054") {
        title
}}`;

export default graphql(query)(MovieInfo);

It sort of works right?

We are querying an id that we hard coded and that's not what we want, instead we want to pass an ID from our react component props to the graphql query. The react-apollo gives us a Query component that enables us to do that.

import { Query, graphql } from 'react-apollo'
class MovieInfo extends Component {
    render(){
        const id = this.props.match.params.id;
        return (
            <Query query={query} variables={{id}} >
            {
                (({loading, err, data}) => {
                    if(loading) return <div>loading</div>
                    return (
                        <div>{data.movieInfo.title}</div>
                    )
                })
            }
            </Query>
        )
    }
}
const query = gql`

query MovieInfo($id: String) {
    movieInfo(id: $id) {
        title
      }
  }
`;

Almost the exact same thing but with Query we are able to pass it variables.

Now let's develop the rest of the component. Inside the Query return the following code

return(
    <div>
        <header style={{backgroundImage:  'url("https://image.tmdb.org/t/p/w500///'+data.movieInfo.poster_path+'")'}}>
            <h2 className="title">{data.movieInfo.title}</h2>
        </header>
        <article className="wrapper">  
            <p className="description">{data.movieInfo.overview}</p>                
            <div className="sidebar">
                <img src={"https://image.tmdb.org/t/p/w500///"+data.movieInfo.poster_path} className="cover_image" alt="" />
                <ul>
                    <li><strong>Genre:</strong> {data.movieInfo.genres}</li>
                    <li><strong>Released:</strong>{data.movieInfo.release_date}</li>
                    <li><strong>Rated:</strong> {data.movieInfo.vote_average}</li>
                    <li><strong>Runtime:</strong> {data.movieInfo.runtime}</li>
                    <li><strong>Production Companies:</strong> {data.movieInfo.production_companies}</li>
                </ul>
                <div className="videos">
                    <h3>Videos</h3>
                    {/* videos */}
                </div>
                    {/* reviews */} 
            </div>
                {/* credits */}                                         
        </article>
    </div>
)

As you can see we are trying to access query properties which we haven't requested. If you run that it will give you a 404 error as the requests fail. Hence, we need to update the query to request more then the title property:

query MovieInfo($id: String) {
    movieInfo(id: $id) {
        title
        overview
        poster_path
        genres
        release_date
        vote_average
        runtime
        production_companies
      }
  }
`;

With that update and with the css that is going to be available in the git repository, the section we've been working on would look something like this:

As you can see in the code comments we need to add videos, reviews and credits on the page.

Adding videos

Remember the way we designed the GraphQL query in part one gives us the ability to fetch the videos within the movieInfo query. Let's do that first:

const query = gql`
query MovieInfo($id: String) {
    movieInfo(id: $id) {
        ...
        videos {
            id 
            key
        }
      }
  }
`;

These videos come as an array - as sometimes there's more then one. So the best way to deal with these arrays is to create a separate method inside the MovieInfo component and let it return all the videos.

class MovieInfo extends Component {
    renderVideos(videos){
        return videos.map(video => {
            return (
                <img key={video.id} 
                    onClick={()=> this.videoDisplay(video.key)} 
                    className="video_thumbs" 
                    src={`http://img.youtube.com/vi/${video.key}/0.jpg`}
                />
            )
        })
    }
    render(){
        ...
        {/* videos */}
        {this.renderVideos(data.movieInfo.videos)}
        ...                     

As we've covered in the first tutorial the key in the videos object refers to the youtube video ID. Youtube gives us the ability to use a screenshot image using that particular format (passed in the src attribute). also, as we previously mentioned, we took the ID exactly because we knew we need something unique for the key - required by React.

When the user clicks on these thumbnail images I want to load a youtube video in the screen, hence onClick={()=> this.videoDisplay(video.key)}. Lets create that functionality.

The way we are going to implement this is by changing the state

class MovieInfo extends Component {
    constructor(){
        super();
        this.state={
            video: null
        }
    }
    videoDisplay(video){
        this.setState({
            video
        })
    }
    videoExit(){
        this.setState({
            video: null
        })
    }
    ...

When the page loads video state is null, then when the thumbnail is clicked and videoDisplay is triggered, video state takes the youtube video key as a value. As we'll see, if the videoExit method is triggered, the video state resets back to null

Finally we need a way to display the video upon state change, so lets create another method. Just under the above methods, add this method:

videoToggle(){
    if(this.state.video) return(
        <div className="youtube-video">
            <p onClick={() => this.videoExit()}>close</p>
            <iframe  width="560" height="315" src={`//www.youtube.com/embed/${this.state.video}` } frameborder="0" allowfullscreen />
        </div>
    ) 
}

Then simply have it render anywhere on the page

<div className="videos">
    {this.videoToggle()}
    <h3>Videos</h3>
    {this.renderVideos(data.movieInfo.videos)}
</div>

Again, if the video state is null, {this.videoToggle()} does nothing. If the state isn't null - if video has a key, then {this.videoToggle()} renders a video.

Adding Movie credits and reviews

I decided to put the movie reviews and movie credits in their own separate component. Let's create the empty component files, import and use them inside the MovieInfo component and also update the query.

Inside ./client/components/MovieInfo.js add these changes

import MovieReviews from './MovieReviews'
import MovieCredits from './MovieCredits'

class MovieInfo extends Component {
...
{/* reviews */}
    <MovieReviews reviews={data.movieInfo.movieReviews} />  
    </div>
        {/* credits */}
        <MovieCredits credits={data.movieInfo.movieCredits} />              
</article>
}
...

const query = gql`

query MovieInfo($id: String) {
    movieInfo(id: $id) {
        ...
        movieReviews {
            id
            content
            author
        }
        movieCredits{
            id
            character
            name
            profile_path
            order
          }
      }
  }
`;
...

We get the data from the movieReviews and movieCredits query, we pass them to their respective components. Now we just quickly display the data

Movie credits component

Add the following code to ./client/components/MovieCredits.js

import React, { Component } from 'react'
export class MovieCredits extends Component {
    renderCast(credits){
        return credits.map(cast => {
            return (
                <li key={cast.id}>
                    <img src={`https://image.tmdb.org/t/p/w500//${cast.profile_path}`} />
                    <div className="castWrapper">
                        <div className="castWrapperInfo">
                            <span>{cast.name}</span>
                            <span>{cast.character}</span>
                        </div>
                    </div>
                </li>
            )
        })
    }
  render() {
    return (<ul className="cast">{this.renderCast(this.props.credits)}</ul>)
  }
}
export default MovieCredits

Nothing new to explain from the above

Movie reviews component

Add the following code to ./client/components/MovieReviews.js

import React, { Component } from 'react'
class MovieReviews extends Component {
    renderReviews(reviews){
        return reviews.map(review => {
            return (
                <article key={review.id}><h4>{review.author} writes</h4>
                    <div>{review.content}</div>
                </article>
            )
        })
    }
    render() {
        return(
            <div className="reviews">
                {this.renderReviews(this.props.reviews)}
            </div>  
        )
    }
} 
export default MovieReviews;

And that's it. This is how the credits, videos and reviews would appear.

Conclusion

The full application, such as it stands can be found at the same repository, and you can view the demo here. It has three branches react-app branch and the master branch have the full code, each tutorial building on top of each other. Where as the Graphql-api branch has the code covered in the first tutorial

Creating a movie website with GraphQL and React (2 Part Series)

1) Creating a movie website with GraphQL and React - part one 2) Creating a movie website with GraphQL and React - part two

Posted on Nov 12 '17 by:

aurelkurtula profile

aurel kurtula

@aurelkurtula

I love JavaScript, reading books, drinking coffee and taking notes.

Discussion

markdown guide
 

Hi. Thanks for reading!

That port is used by livereload. It's not our problem, you don't need to use that. Everything we need is at port 4000

http://localhost:4000/graphql // for graphql
http://localhost:4000 // for the site

I read the tutorial again and try to make it clear that we are working in port 4000.

Thanks for the question. Let me know if that answers the question

(To test this I cloned the master branch as well and added the api key to .env)

I have babel-cli installed globally and always take it for granted to mention it :)