Building a movie watchlist app with React

React is intriguing. It has a much smaller footprint than a full blown Javascript framework like Angular, it's component based and relies on one-way data flow. The JSX syntax is very different from what I'm used to, but after giving it a try having the markup and related Javascript code all in one place has some advantages to it.

To start with React there is a bit of background information to dig through. React.js for stupid people explains the basics in most simple terms possible and helped me get started. Don't let the title of the post put you off, it's not exclusively for stupid people. :)


Let's build a movie watchlist app with React

For my little ‘watchlist’ application I used React, Alt.js as a flux implementation, reqwest for making Ajax requests. The components take advantage of a few new ES6 features and are tranformed/bundled with help of Babel and Browserify. The bare bones interface was enhanced with help of Semantic UI pulled from CDN.

React is backend agnostic, it will integrate well with any framework that can expose an API. I made it talk to Express.js + MongoDB and Laravel + MySQL.

Here's how the ‘watchlist’ app looks like:


movie watchlist app


Thinking in terms of components

The top level component is called App. It listens to MovieStore and passes initial data to lower level components.

import React from 'react';
import AddMovieField from './AddMovieField.js';
import MovieList from './MovieList.js';
import MovieStore from '../stores/MovieStore';
import MovieActions from '../actions/MovieActions';
import connectToStores from 'alt/utils/connectToStores';

class App extends React.Component {
  static getStores() {
    return [MovieStore];
  }

  static getPropsFromStores() {
    return MovieStore.getState();
  }

  componentDidMount() {
    // Loading initial data from server
    MovieActions.getMovies();
  }

  render() {
    // We are going to pass a subset of movies (unwatched) to MovieList component
    let movies = this.props.movies.filter((movie) => {
      return !movie.watched;
    });

    // We are going to pass a subset of movies (watched) to MovieList component
    let watchedMovies = this.props.movies.filter((movie) => {
      return movie.watched;
    });

    return (
      <div className="ui grid container">
        <div className="row">
          <div className="ui single column stackable">
            <AddMovieField id="new-movie" placeholder="Movie..." />
          </div>
        </div>
        <div className="row">
          <div className="column">
            <div className="ui two column grid stackable">
              <MovieList title="Watchlist" movies={movies} />
              <MovieList title="Watched" movies={watchedMovies} />
            </div>
          </div>
        </div>
      </div>
    );
  }
}

module.exports = connectToStores(App);

The reusable MovieList component allows to display watched & unwatched movies.

The data comes from the App component as props. The filtered movies are stored as part of the component state.

import React from 'react';
import Movie from './Movie';

class MovieList extends React.Component {
  constructor(props) {
    super(props);
    /*
     * The data (movies) comes from the higher level component as props. We are
     * going to use state to manage filtering of movies.
     */
    this.state = {
      filteredMovies: [],
      filtering: false,
      keyword: ''
    };
  }

  componentWillReceiveProps(nextProps) {
    if (this.state.filtering) {
      /*
       * If the movies got deleted or watched/unwatched we have to reflect it
       * in the filteredMovies array.
       */
      this.setState({ filteredMovies: this.filterMovies(nextProps.movies) });
    }
  }

  filterMovies(movies) {
    return movies.filter((movie) => {
      // search by keyword
      return movie.title.toLowerCase().search(this.state.keyword.toLowerCase()) !== -1;
    });
  }

  filterList(event) {
    let keyword = event.target.value;
    if (!keyword) {
      this.setState({ filtering: false });
      return;
    }
    this.setState({
      filteredMovies: this.filterMovies(this.props.movies),
      filtering: true,
      keyword: keyword
    });
  }

  render() {
    let movieList = [];
    let movies = this.state.filtering ? this.state.filteredMovies : this.props.movies;

    if (movies.length) {
      movies.map((movie) => {
        movieList.push(<Movie key={movie._id} movie={movie} />);
      });
    }

    if (this.props.movies.length == 0) {
      return (
        <div className="column">
          <h4>{this.props.title}</h4>
          No movies on this list.
        </div>
      );
    }

    return (
      <div className="column">
        <h4>
          {this.props.title}  
          <div className="ui label basic circular">
            {this.state.filtering ?
              'Showing ' + this.state.filteredMovies.length
                + ' out of ' + this.props.movies.length : this.props.movies.length}
          </div>
        </h4>
        <div className="ui top attached menu">
          <div className="right menu">
            <div className="ui right aligned search item">
              <div className="ui transparent icon input">
                <input
                  className="prompt"
                  placeholder="Search movies..."
                  onChange={this.filterList.bind(this)}
                />
                <i className="search link icon"></i>
              </div>
            </div>
          </div>
        </div>
        <div className="ui bottom attached segment">
          <div className="ui divided items">
            {movieList.length ? movieList : 'Nothing found.'}
          </div>
        </div>
      </div>
    );
  }
}

Movie.propTypes = {
  movies: React.PropTypes.array,
  title: React.PropTypes.string
};

module.exports = MovieList;

The Movie component calls actions that will manage the movie's state. To see how data flows when the movie's state changes see MovieActions and MovieStore (not included here for the sake of brevity).

import React from 'react';
import MovieActions from '../actions/MovieActions';

class Movie extends React.Component {
  toggleWatched() {
    MovieActions.toggleWatched(this.props.movie._id, !this.props.movie.watched);
  }

  destroy() {
    MovieActions.destroy(this.props.movie._id);
  }

  render() {
    return (
      <div className="item">
        <div className="middle aligned content">
          {this.props.movie.title}
        </div>
        <div className="middle aligned content">
          <button
            className="tiny ui right floated red button"
            onClick={this.destroy.bind(this)}>
            Delete
          </button>
          <button
            className="tiny ui right floated teal button"
            onClick={this.toggleWatched.bind(this)}>
            {this.props.movie.watched ? 'Unwatch' : 'Watched'}
          </button>
        </div>
      </div>
    );
  }
}

Movie.propTypes = {
  movie: React.PropTypes.object.isRequired
};

module.exports = Movie;

AddMovieField will add a new movie to the list on submit or key press (Enter):

import React from 'react';
import MovieActions from '../actions/MovieActions';

var ENTER_KEY_CODE = 13;

class AddMovieField extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: this.props.value || ''
    }
  }

  // Updating state as the user types
  onChange(event) {
    this.setState({
      value: event.target.value
    });
  }

  onKeyDown(event) {
    if (event.keyCode === ENTER_KEY_CODE) {
      this.saveMovie();
    }
  }

  saveMovie() {
    if (!this.state.value) return;
    MovieActions.create(this.state.value);
    this.setState({ value: '' });
  }

  render() {
    return (
      <div className="ui right action input">
        <input
          placeholder={this.props.placeholder}
          value={this.state.value}
          onChange={this.onChange.bind(this)}
          onKeyDown={this.onKeyDown.bind(this)}
          autoFocus={true}
        />
        <a className="ui teal button" onClick={this.saveMovie.bind(this)}>
          <i className="add icon"></i>
          Add
        </a>
      </div>
    );
  }
}

AddMovieField.propTypes = {
  placeholder: React.PropTypes.string
};

module.exports = AddMovieField;

Finally, here's how the App component gets rendered:

import React from 'react';
import App from './components/App';

React.render(<App />, document.getElementById('app'));


Final comments & source code

My first impressions from using React are pretty positive. It enforces a clean structure based around components. The full source code of the ‘watchlist’ app is available on GitHub:

Watchlist with Express.js/MongoDB API

Watchlist with Laravel/MySQL API


What next?

One area I didn't look into yet is server side rendering. Express.js can do server side rendering together with React. I was also happy to discover react-laravel package that brings server side rendering of React components to Laravel.

Did you like this post?
Previous post

The most inspiring talks of UX Poland 2015 — Experience Showroom

This year I had an opportunity to visit UX Poland conference and I was blown away by the positive vibe of the event in the beautiful venue of Jablkowski Brothers Department Store in Warsaw.
Next post

Laravel Clyde — image uploads, resizing & manipulations for Laravel

It's very common for web applications to deal with image uploads, image resizing and other manipulations. Arguably the best approach to the problem (apart from offloading the task to a service like imgix) is to store the uploads in the cloud and resize and manipulate the images on the fly.