4

I am working on movie collection app , where i have a page with movie list. I want to add the functionality of filtering the movie based on year, genre and rating and also i want to sort it alphabetically ,year and rating. How can i accomplish that by using redux and react by using single state object.

1
  • * doesn't have enough reputation to make comment key={i} and sort in getVisibleMovies will cause entire list and list item to re-render Commented Jan 27, 2017 at 19:38

1 Answer 1

32

First, we should create component to display movies list.
Note, this is a pure stateless component, we are not making sorting or filtering there, just rendering derived props:

// Movies.js

import React from 'react';

function Movies({ movies }) {
  return (
    <ul>
      {movies.map((m, i) =>
        <li key={i}>{m.year} - {m.title}.({m.genre}) - {m.rating}</li>
      )}
    </ul>
  );
}

export default Movies;

Next we will create another stateless component, which will contain sorting and filters view.
Here we are also rendering props, and providing callback to select elements:

// Pane.js

import React from 'react';

function Pane({
  selectedYear,
  selectedGenre,
  selectedRating,
  years = [],
  genres = [],
  ratings = [],
  sorting,
  onYearChange,
  onGenreChange,
  onRatingChange,
  onSortingChange,
}) {
  return (
    <div>
      <div>
        Filters:
        <div>
          Year:
          <select
            defaultValue={selectedYear}
            onChange={e => onYearChange(e.target.value)}
          >
            <option value="all" >All</option>
            {years.map((y, i) =>
              <option key={i} value={y}>{y}</option>
            )}
          </select>
        </div>
        <div>
          Genre:
          <select
            defaultValue={selectedGenre}
            onChange={e => onGenreChange(e.target.value)}
          >
            <option value="all" >All</option>
            {genres.map((g, i) =>
              <option key={i} value={g}>{g}</option>
            )}
          </select>
        </div>
        <div>
          Rating:
          <select
            defaultValue={selectedRating}
            onChange={e => onRatingChange(e.target.value)}
          >
            <option value="all" >All</option>
            {ratings.map((r, i) =>
              <option key={i} value={r}>{r}</option>
            )}
          </select>
        </div>
      </div>
      <div>
        Select sorting:
        <select
          defaultValue={sorting}
          onChange={e => onSortingChange(e.target.value)}
        >
          <option value="alphabetically">Alphabetically</option>
          <option value="year">Year</option>
          <option value="rating">Rating</option>
        </select>
      </div>
    </div>
  );
}

export default Pane;

We will store movies and all filtering and sorting data in app state. We need create reducer, to handle sorting and filtering actions:

// reducers.js

const items = [{
  title: 'Mad max',
  year: 2015,
  rating: 8,
  genre: 'fantasy',
}, {
  title: 'Spider man 2',
  year: 2014,
  rating: 7,
  genre: 'fantasy',
}, {
  title: 'Iron man 3',
  year: 2013,
  rating: 7,
  genre: 'fantasy',
}, {
  title: 'Dumb and Dumber To',
  year: 2014,
  rating: 5,
  genre: 'comedy',
}, {
  title: 'Ted 2',
  year: 2015,
  rating: 6,
  genre: 'comedy',
}];

export default function moviesReducer(state = {
  movies: items,
  year: 'all',
  rating: 'all',
  genre: 'all',
  sorting: 'year',
}, action) {
  switch (action.type) {
    case 'SET_YEAR':
      return {
        ...state,
        year: action.year,
      };
    case 'SET_RATING':
      return {
        ...state,
        rating: action.rating,
      };
    case 'SET_GENRE':
      return {
        ...state,
        genre: action.genre,
      };
    case 'SET_SORTING':
      return {
        ...state,
        sorting: action.sorting,
      };
    default:
      return state;
  }
}

To provide data to stateless components, we should use containers.
Let's create PaneContainer to provide sorting and filtering data to Pane component:

// PaneContainer.js

import { connect } from 'react-redux';
import Pane from './Pane';

// Simple helper function, which return all filters from state by given key.
function getFilters(key, movies) {
  return movies.reduce((acc, movie) => {
    if (!acc.includes(movie[key])) {
      return [...acc, movie[key]];
    }
    return acc;
  }, []);
}

function mapStateToProps(state, props) {
  const { sorting, year, genre, rating } = state;
  return {
    selectedYear: year,
    selectedGenre: genre,
    selectedRating: rating,
    years: getFilters('year', state.movies),
    genres: getFilters('genre', state.movies),
    ratings: getFilters('rating', state.movies),
    sorting,
  };
}

function mapDispatchToProps(dispatch, props) {
  return {
    // Here, we are providing callbacks with dispatching functions.
    onYearChange(year) {
      dispatch({
        type: 'SET_YEAR',
        year,
      });
    },
    onGenreChange(genre) {
      dispatch({
        type: 'SET_GENRE',
        genre,
      });
    },
    onRatingChange(rating) {
      dispatch({
        type: 'SET_RATING',
        rating,
      });
    },
    onSortingChange(sorting) {
      dispatch({
        type: 'SET_SORTING',
        sorting,
      });
    },
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Pane);

And finally, we will create MoviesContainer which will provide visible movies to Movies component:

// MoviesContainer.js

import { connect } from 'react-redux';
import Movies from './Movies';

// Getting visible movies from state.
function getVisibleMovies(year, genre, rating, sorting, movies) {
  return movies
    .filter(m => {
      return (
        (year == 'all' || year == m.year) &&
        (genre == 'all' || genre == m.genre) &&
        (rating == 'all' || rating == m.rating)
      );
    })
    .sort((a, b) => {
      if (sorting == 'year') {
        return b.year - a.year;
      }
      if (sorting == 'rating') {
        return b.rating - a.rating;
      }
      if (sorting == 'alphabetically') {
        return a.title > b.title ? 1 : a.title < b.title ? -1 : 0;
      }
    });
}

function mapStateToProps(state) {
  const { year, genre, rating, sorting, movies } = state;
  return {
    movies: getVisibleMovies(year, genre, rating, sorting, movies),
  };
}

export default connect(mapStateToProps)(Movies);
Sign up to request clarification or add additional context in comments.

3 Comments

Where is the movies container? You imported the Movies component and then connect it to the redux store. This doesn't look like a good practice. Does this code even work?
@IsaacPak Movies component is defined in the beginning of the answer. MoviesContainer is defined in the end. Using connect, to provide the data from redux store to component is a common approach.
Ok. I have been connecting my containers to the store. But now that i've reviewed these docs, your code makes more sense now. Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.