Movie Listings Application with React-Router v4

In my previous article Getting Rowdy with React Router we outlined quite a few changes to React Router and what that means for us. If you're interested in upgrading your already existing application, this article can be used as a reference sheet for ideas. If you're starting brand new, this would get you up and running with the latest version of React Router.

Instead of walking through setting up Webpack, React, servers, and whatever cool new JavaScript thing there is, we're going to rely on create-react-app! Check out Create a New React Application the Easy Way if you're not familiar.

Whew! All right, we're done with referencing articles. Let's start introducing a SPA router to our React application!

Generating Our Application

1.0 create-react-app

First things first, let's generate this application -- we'll call it Flix! This will give us a slightly barebones application that we can use to demonstrate the effectiveness of React Router.

$ npm install -g create-react-app
$ ... things are installing
$ ... DONE
$ cd ~/whateverFolderYourProjectsLiveIn/
$ create-react-app flix

Without getting too technical on what create-react-app is doing, I'm going to show you the helpful commands that were placed in our package.json

...
"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test --env=jsdom",
  "eject": "react-scripts eject"
}

Great! Now we're able to start, build, test, and eject our application at any moment. Wasn't that easy?

$ npm run start

Your terminal should clear and you should see a happy green Compiled successfully! while the start script opens a browser to show you the newly generated application. It should look something like this:

Nifty, we're finished with generating and can move on to writing our application.

Creating a Movie Listings Application

  • Remove Boilerplate
  • Setup React-Router
  • Adding Data
  • Routing
  • Tests

Removing Boilerplate

2.0 Fresh Start

All right, it's not that much better than a todo list, but at least we'll get to think about watching movies. Now that we're all ready to go, let's remove the default React boilerplate that was provided to us.

Navigate to src/App.js remove some code just to make it presentable for our application. Depending on the version of create-react-app, you may not have this exact code, but that's okay! All we're doing is changing a few lines to get the visuals going.

This is what was generated from create-react-app. Let's change the h2 our application name of Flix and remove the logo and paragraph.

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>

        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

After we do the changes suggested above we should have something like this:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <h2>Flix</h2>
        </div>
      </div>
    );
  }
}

export default App;

Great, we're as barebones as we can be (even though we scaffolded). We should be able to start installing packages and making this our own application!

Building Our Application

3.0 Styles

This is not required, but if you're following along and your application is looking a little sad, copy and paste these styles into your src/index.css. This will make your eyes feel better.

3.1 Project Structure

Now that we're getting into React land, we may want a little more structure than what create-react-app has provided us. Currently we're limited to a src directory where all of our JS, CSS, and images live. I tend to gravitate towards a separate directory for each section of the application. Let's set that up.

$ cd src/
$ mkdir components && cd components

Our src directory tree should now look like this:

src/
  components/
  App.js
  App.test.js
  index.css
  index.js
  logo.svg

If we're going to add additional sections to the application, we would likely create a directory inside components/ that contains components directly pertinent to the section. For now, let's place our components as .js files inside the components directory.

3.2 Project Dependencies

We need to install the react-router latest version, and at this time it is v4.0.0-alpha.4. Make sure you're in your project directory when you install react-router.

Note: If you're running npm v2.15.9 or less you may be required to upgrade to v3. This may prevent you from installing react-router due to unmet peer dependencies. Upgrading npm will resolve this: npm install -g npm.

$ npm install --save react-router@4.0.0-alpha.4

Verify that your package.json has the installed package.

...
"dependencies": {
  "react": "^15.4.0-rc.4",
  "react-dom": "^15.4.0-rc.4",
  "react-router": "^4.0.0-alpha.4"
},
...

Looks good, we can see that the package is saved to our dependencies.

Thinking a little bit ahead, we know we're showing movies and your thought might be how are we getting this data? For the sake of brevity, we're going to create a movies.json file with our data. This will act as if we're pulling information from an API or database.

Let's go ahead and create a file named movies.json inside our src directory.

$ cd src/
$ touch movies.json

Then we can copy & paste this data in there and forget about it forever. Of course you can add to it if you'd like, but for now these movies will illustrate the point.

3.3 Home Page

Okay, let's start routing! Because we're creating a movie listings application I thought we could start by adding listings of movies on the front page. To do that, we're going to have to configure our root route to render a Home component. In 3.0 Project Structure, we created a directory named components, which is where our components for our home will live.

$ cd src/components/
$ touch Home.js

Now that the file is created, let's create a featured movies section on our home page that highlights the top 4 movies. I don't think we'll need any React lifecycle methods, so let's go ahead and create a stateless Home component.

We'll show a header and then iterate through our movies collection and show the top 4 inside a FeaturedMovies component.

import React from 'react';
import movies from '../movies.json';
import FeaturedMovie from './FeaturedMovie';

const Home = () => {
  const topFour = movies.slice(0, 4);

  return (
    <div>
      <h2 className="featured-movies__header">
        Featured Movies
      </h2>

      <hr />

      <div className="featured-movies">
        {topFour.map((movie, i) => (
          <FeaturedMovie
            movie={movie}
            key={i}
          />
        ))}
      </div>
    </div>
  );
};

export default Home;

Note: If you're a little new to React, you'll probably be asking why we're attaching a key prop to FeaturedMovie. In React, when we're iterating over a collection, React wants to know the uniqueness of the element. When we attach a key to the element, React more or less "tracks" this and will keep the identity. If there was no key defined, React would re-render this element on every single render.

Great! This is starting to shape up. You should be getting a compile error telling you that FeaturedMovie component does not exist. Let's fix this.

$ cd src/components/
$ touch FeaturedMovie.js

Based on our Home component all we're doing is creating a presentation component for featured movies. This component will take a movie and display the information about it! Like before, we don't need the React lifecycle methods, so we're free to create this as a stateless component.

import React, { PropTypes } from 'react';

const FeaturedMovie = ({ movie }) => (
  <div className="featured-movie">
    <div className="featured-movie__image">
      <img alt={movie.name} src={movie.image} />
    </div>

    <div className="featured-movie__info">
      <p><b>{movie.name}</b></p>
      <p>{movie.director}</p>
      <p>{movie.released}</p>
    </div>
  </div>
);

FeaturedMovie.propTypes = {
  movie: PropTypes.shape({
    director: PropTypes.string.isRequired,
    name: PropTypes.string.isRequired,
    released: PropTypes.string.isRequired,
    image: PropTypes.string.isRequired,
  }).isRequired,
};

export default FeaturedMovie;

Note: PropTypes in React are used to check the data coming through to a component. If we declared movie as a string and we gave FeaturedMovie an object, it would result in a Warning: Failed prop type: Invalid prop 'movie' of type 'object' supplied to 'FeaturedMovie' expected 'string'. The application will still run but could result in actual errors if your component requires a certain type.

Okay, recapping a little bit, we've created a Home component that will have a header, iterate through our movie collection, and show the FeaturedMovie component. The only thing that's left for this piece is wiring up the root route of the application to display our Home component.

React Router provides three things we'll need to make this work. You can read their docs if you're confused.

  • <BrowserRouter>: Keeps UI in sync with the browser history.
  • <Link>: Provides declarative navigation around the application.
  • <Match>: Renders UI when the pattern matches the location.

Below are the steps we need to follow to render our Home component on the root of our application.

  1. We'll wrap our application in a <BrowserRouter> component so we can keep the UI in sync with the browser history.
  2. Add the navigation <Link> component to direct our browser to /.
  3. Add the <Match> component to render a component whenever the browser location matches /.
import React, { Component } from 'react';
import './App.css';

import Home from './components/Home';

import Router from 'react-router/BrowserRouter';
import Match from 'react-router/Match';
import Link from 'react-router/Link';

class App extends Component {
  render() {
    return (
      <Router>
        <div className="App">
          <div className="App-header">
            <h2>Flix</h2>
            <Link to="/">Home</Link>
          </div>

          <div className="container">
            <Match exactly pattern="/" component={Home} />
          </div>
        </div>
      </Router>
    );
  }
}

export default App;

We've added <Router> as the first child of our render method and nested all of our code inside it. This will ensure everything that is a child of our App component will have direct access to routing. Additionally, we've added a navigation link that will tell the router where we want to direct the user. After a user has clicked the navigation link, React Router notices we have a <Match> component that is looking for the exact pattern of / and will render the Home component in that exact place.

Here's a preview of where we are at this point. We have featured movies rendering on the home page of our application.

3.4 Single Movie Page

Now, we should be ready to click into the details of each movie. I'd like to know what the reviews are before I decide which one to watch.

Before we dive into creating the component, let's setup the routing. In our src/App.js we need to create another <Match> for this upcoming route. Remember, if the pattern matches, it will immediately render that component. In our case, the Home component will not be loaded when the URL matches /movies/:movieId. Only the individual movie component will load.

There is a possibility that we can render two components that are separate from each other — for instance, what if we wanted to show the individual movie details under the Featured Movies section? This is a valid use case, but we're not covering that in this article.

All right, let's add a <Match> to src/App.js

...
<div className="container">
  <Match exactly pattern="/" component={Home} />
  <Match pattern="/movies/:movieId" component={Movie} />
</div>
...

We've added the pattern of /movies/:movieId with a component named Movie that will load when the pattern matches.

I think we're also going to need to link the image of the Featured Movie to this individual movie component. Let's add that link in the FeaturedMovie component.

import Link from 'react-router/Link';

...
<div className="featured-movie__image">
  <Link to={`/movies/${movie.id}`}>
    <img alt={movie.name} src={movie.image} />
  </Link>
</div>
...

Here we've imported Link from React Router and then wrapped our img tag with that component. We used the to property to tell React Router where we want to send the user when they click this image. It should change the URL to /movies/:movieId, and then our Home component should pick up the change and render the Movie component.

Now that we've created the Match and the Link, we're on to writing the component!

$ cd src/components/
$ touch Movie.js

After creating the file, let's talk about the data we're trying to grab here. In a React application that uses Redux, we could grab this individual movie we've clicked into from the application's state. However, we don't have Redux. We have a data file that is simulating an API, DB, or Redux state tree. We'll have to find the movie in our movies.json and render that information.

import React, { PropTypes } from 'react';
import movies from '../movies.json';

const Movie = ({ params: { movieId } }) => {
  const movie = movies.find(
    movie => movie.id === parseInt(movieId, 10)
  );

  return (
    <div>
      <div className="movie-title">
        <h2>{movie.name}</h2>

        <hr />
      </div>

      <div className="movie-container">
        <div className="movie-image">
          <img src={movie.image} />
        </div>

        <div className="movie-information">
          <p><b>Director:</b> {movie.director}</p>
          <p><b>Release Date:</b> {movie.released}</p>
          <p><b>Description:</b> {movie.description} </p>
        </div>
      </div>

      <div className="movie-reviews">
        <h2>Reviews</h2>

        <hr />

        {movie.reviews.map((review, i) => (
          <div key={i} className="movie-review">
            <h3>
              {review.title} <span className="review-author">by {review.author}</span>
            </h3>

            <p>{review.body}</p>
          </div>
        ))}
      </div>
    </div>
  );
};

Movie.propTypes = {
  params: PropTypes.shape({
    movieId: PropTypes.string.isRequired,
  }).isRequired,
};

export default Movie;

Going quickly over the code, we're creating another stateless component that takes params as an argument, and we destructure that object to grab the movieId that's in the Link from FeaturedMovie. As soon as we have the id of the movie we're looking up, we can create a variable named movie and find the film in our movies.json.

You may notice we're using parseInt on the movieId parameter. This is passed over as a string from the URL pattern. In order to compare and find the movie in our movies.json, we could either use parseInt with strict equality or type-converting equality such as ==. If we tried to use strict equality without parseInt, we wouldn't find our movie!

After we've found our individual movie in our dataset, we can show the details as normal. We should be able to navigate to individual movie pages now! Try clicking one of the featured movies on the home page.

3.5 Movies Page

We're almost there. We've added a home page with featured movies and an individual movie details page. I think it's time to add a listing of all the movies we host at Flix. Let's add a Link, Match, and import in our App.js to prepare our application for the component.

import Movies from './components/Movies'; // import our component

...

<div className="App-header">
  <h2>Flix</h2>

  <Link to="/">Home</Link>
  <Link to="/movies">Movies</Link> // add link to new component
</div>

...


<div className="container">
  <Match exactly pattern="/" component={Home} />
  <Match exactly pattern="/movies" component={Movies} /> // add a match to new component
  <Match pattern="/movies/:movieId" component={Movie} />
</div>
...

Nothing out of the ordinary. Like before, all we need to do is setup a Link, Match, and a rendered component. Let's create this last component!

$ cd src/components/
$ touch Movies.js
import React from 'react';
import Link from 'react-router/Link';
import movies from '../movies.json';

const Movies = () => (
  <div>
    <h2 className="movies-header">
      Movies
    </h2>

    <hr />

    <div className="movie-listings">
      {movies.map((movie, i) => (
        <div key={i}>
          <div className="movie-image">
            <Link to={`/movies/${movie.id}`}>
              <img alt={movie.name} src={movie.image} />
            </Link>
          </div>
        </div>
      ))}
    </div>
  </div>
);

export default Movies;

Similar to our FeaturedMovies component, we're importing the movies from our dataset movies.json and then iterating over the entire collection. We've wrapped the movie image in a Link that directs the user to the individual movie page. That should sum it up!

3.6 Adding Helpful Features

We've created an application that has three routes /, /movies, and /movies/:movieId. What if a user came to our application and decided they wanted to visit flix.com/battlestar-galactica? I wish we could ignore the possibility of user's making up their own URLs. Unfortunately we can't! React Router provides a helpful <Miss> component for this purpose. First, it will try to find a match for the URL pattern and then decide "Welp, nothing here." and render the <Miss> if it is available. Let's add this in our App.js!

...

import Home from './components/Home';
import Movie from './components/Movie';
import Movies from './components/Movies';
import PageNotFound from './components/PageNotFound'; // import of new component

import Router from 'react-router/BrowserRouter';
import Match from 'react-router/Match';
import Link from 'react-router/Link';
import Miss from 'react-router/Miss'; // import react-router's Miss component

...

<div className="container">
  <Match exactly pattern="/" component={Home} />
  <Match exactly pattern="/movies" component={Movies} />
  <Match pattern="/movies/:movieId" component={Movie} />

  <Miss component={PageNotFound} /> // add Miss component with our PageNotFound component
</div>

...

Adding the PageNotFound component.

import React from 'react';

const PageNotFound = () => (
  <div className="page-not-found">
    We're sorry. This page doesn't exist!
  </div>
);

export default PageNotFound;

Give it a try! Try to navigate to that battlestar-galactica URL. You should be viewing your PageNotFound component!

Conclusion

There you have it: a simple movie listings application that uses React Router to serve basic routes. In the next part of this series, Introducing Authentication to Your SPA, we will add protected and public routes using React Router and some strategies involved with authentication! This application is hosted on GitHub, and you may use it however you like.

Sources