In this post, Redux state implementation will be demonstrated within React application using immutable.js, normalizr and reselect.

Goal

Build a React application which lists movies from nested API response. Movies can be filtered against genre or actor.

What is used ?

React, Reduximmutable.js, normalizr, reselect

Code

Whole application code can be checked via this repository: https://github.com/fatihAydinoglu/redux-example

React and Redux

In this blog post, we will concentrate on tools which can be used with Redux state. It is assumed that you already know about React and Redux. If not, please check some tutorials about them first.

You can check “/components” folder to see how React components are created. Basically, there are two connected container components:

  1. MoviesFilter: Gets genres, actors and current selected filter from redux state. Creates filter components. Handles filter selections.
  2. MoviesResult: Gets filtered movies result from state. When mounted, get movies from API. (In this basic example, it is mock response.)

Normalizr

It normalizes nested JSON according to a schema. It is very helpful if your API response is deeply nested.

Our mock movie response : moviesResult.js As you can see, we have movies, every movie has title and nested genres and actors list. Please check how genres and actors are repeated in result.

At fetchMovies action we send normalized result to our reducer.

// src/actions.js
const fetchMovies = () => ({
  type: 'FETCH_MOVIES',
  payload: movieNormalizer(moviesResult), // normalize result
});

To normalize our API response, we simply define our schema and call normalize function from normalizr.

// src/movieNormalizer.js
import { schema, normalize } from 'normalizr';
// schema definitions
const actor = new schema.Entity('actors');
const genre = new schema.Entity('genres');
const movie = new schema.Entity('movies', {
  genres: [genre],
  actors: [actor],
});
const movieList = [movie];
// Converts moviesResult to normalizedMoviesResult
const movieNormalizer = moviesResult => normalize(moviesResult, movieList);
export default movieNormalizer;

After normalize our movies result will look like this. Now, we have result array and genre, actors and movie array seperately.

Immutable.js

Initial Redux state is created with immutable.js. By this, we ensure that our state can not be mutated.

// src/reducer.js
import { fromJS } from 'immutable';
const initialState = fromJS({ filter: { genre:'', actor:''} });

And also, reducer function uses immutable.js functions to return new state. See “mergeDeep” functions below:

// src/reducer.js
const reducer = (state = initialState, action) => {
  switch (action.type) {
    // handle fetch movies action
    case 'FETCH_MOVIES':
      return state.mergeDeep(action.payload);
    // handle set filter action
    case 'SET_FILTER':
      return state.mergeDeep({ filter: action.filter });
    default:
      return state;
  }
};

We ensure that Redux state holds immutable and normalized API response data.

Reselect

Reselect is a selector library for Redux. Container components will get data from selectors not state directly. Selector is efficient way to get data from state because a selector is not recomputed unless one of its arguments change.

Please check how data is selected with immutable.js functions “get” and “getIn“:

// src/selectors.js
// Select entities from state
const getResult = state => state.get('result');
const getGenres = state => state.getIn(['entities', 'genres']);
const getActors = state => state.getIn(['entities', 'actors']);
const getMovies = state => state.getIn(['entities', 'movies']);
// Select filter from state
const getFilter = state => state.get('filter');

Please also check how selectors are composed together:

// src/selectors.js
const getMoviesResult = createSelector(
  getResult,
  getMovies,
  getGenres,
  getActors,
  getFilter,
  (result, movies, genres, actors, filter) => {
    if (!result || result.size === 0) return null;
    return (
      result
        // filter results first
        .filter((movieId) => {
          const movie = movies.get(movieId.toString());
          const genre = filter.get('genre');
          const actor = filter.get('actor');
          return (
            (genre === '' || movie.get('genres').includes(Number(genre))) &&
            (actor === '' || movie.get('actors').includes(Number(actor)))
          );
        })
        // return movie results with actors and genres
        .map((movieId) => {
          const movie = movies.get(movieId.toString());
          const movieResultItemMap = Map({
            movie,
            actors: movie.get('actors').map(actorId => actors.get(actorId.toString())),
            genres: movie.get('genres').map(genreId => genres.get(genreId.toString())),
          });
          return movieResultItemMap;
        })
    );
  },
);

You can use selectors in any connected container component like “MoviesFilter.js“. You just need to assign result of selectors to pros with “mapStateToPros“:

// src/components/MoviesFilter.js
import { getGenres, getActors, getFilter } from '../selectors';
...
function mapStateToProps(state) {
  return {
    genres: getGenres(state),
    actors: getActors(state),
    filter: getFilter(state),
  };
}

// connect to redux
export default connect(mapStateToProps, {
  setFilter,
})(MoviesFilter);

After connecting to redux, you can use “this.props.genres” or “this.props.actors” like any other props.

Conclusion

If you clone repository and run it, you can see these tools in action. You can list and filter movies from normalized and immutable redux state with reselect.

  • Zarko Dencic

    Hello, thank you for the article, I find it very helpful. I am wandering is there performance drawback of using toJS inside of mapStateToProps since redux docs are warning us about this:
    https://redux.js.org/docs/recipes/UsingImmutableJS.html#never-use-tojs-in-mapstatetoprops ? You use toJS in selectors which are called from mapStateToProps.

    • Fatih Aydinoglu

      Hi Zarko,

      Thanks for your warning.
      You are absolutely right. It was not a proper usage of selectors with immutable objects.

      I have removed toJS calls.
      I will check this repo again when I have time for further improvements.