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 totrue
whilst data is fetched, andfalse
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
Top comments (2)
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
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 :)Fantastic.