Build a Retrogames Archive with Node.JS, React, Redux and Redux-Saga Part2: Redux Integration.

Samuele Zaza

In the previous tutorial we built the retrogames archive app and successfully made it work.

Actually, for a small project like this, adding Redux may increase the overall complexity of the code without real benefits. At most we could do some refactoring, create some utils functions and so on. However we can consider the project as the base for a more complex one so Redux improves the overall development experience as well as the code organization:

By decoupling the state from the our components we will see immediate benefits in terms of readibilty, moreover we have a precise picture of the current state. No more non-deterministic state means easy debugging as well!

In few words I summarized a few reasons why I like Redux and why I use it in my apps:

  • Decouple the state from the components. By connecting the containers to the redux store I can get the data I need and pass it as props to presentational components. This helps readibility.
  • unidirectional data-flow rocks!
  • Having a single state means having a single source of truth.
  • It's developer friendly, with redux-dev-tools debugging my code is easier and faster.
  • I can do time travelling by deleting previously dispatched actions.
  • Redux comes with great documentation.

For more, take a look a the documentation on "when should I use Redux?".

Prerequisites

  • The most obvious is having the code of the part.1 that you can grab on my github.
  • The prerequisites of the part.1 are also still valid, plus I assume some basic knowledge of Redux and Immutability.

Regarding the project, if you want to start from the part1 and update the project incrementally, you can get the code on github and checkout to tutorial/part1 branch to start editing the code. On the other hand you can just checkout to tutorial/part2 branch instead to get the exact code of this tutorial.

Table of Contents

Folder Structure

That's the folder structure for the Part.2:

 --app
 ----models
 ------game.js
 ----routes
 ------game.js
 --client
 ----dist
 ------css
 --------style.css
 ------fonts
 --------PressStart2p.ttf
 ------index.html
 ------bundle.js
 ----src
 ------actions
 --------filestack.js
 --------games.js
 ------components
 --------About.jsx
 --------Archive.jsx
 --------Contact.jsx
 --------Form.jsx
 --------Game.jsx
 --------GamesListManager.jsx
 --------Home.jsx
 --------index.js
 --------Modal.jsx
 --------Welcome.jsx
 ------constants
 --------filestack.js
 --------games.js
 ------containers
 --------AddGameContainer.jsx
 --------GamesContainer.jsx
 --------reducers
 ----------filestack.js
 ----------games.js
 ----------index.js
 --------sagas
 ----------filestack.js
 ----------games.js
 ----------index.js
 ------index.js
 ------routes.js
 ------store.js
 --.babelrc
 --package.json
 --server.js
 --webpack-loaders.js
 --webpack-paths.js
 --webpack.config.js
 --yarn.lock

Rewrite GamesContainer

To better understand the process of integrating Redux in our app we can start from the games list view.

Take a look at GamesContainer function getGames:

/* ... code */
// This is the fetch code directly in the container
getGames () {
    fetch('http://localhost:8080/games', {
      headers: new Headers({
        'Content-Type': 'application/json'
      })
    })
    .then(response => response.json())
    .then(data => this.setState({ games: data }));
  }
  /* ... code */

We are making an asynchronous call to the server to fetch our games and then put them in the state. Thus, this is a perfect candidate for our purpose.

Let's think about the new state structure, here is an initial draft the games list only:

games : { 
    list : [
        {//...Game1},
        {//...Game2},
        ...
    ]
}

We added one more level (list) compared to the original one, this is will come handy once our state grows.

We need to install a few packages, let's start from Redux and Immutable (Our state will be an immutable data-structure):

yarn add redux immutable

Actions

First, we write the actions so create the action folder in /client/src and inside create new a file called games.js which contains our action creators. Then, paste the following code:

// We import the constants from a /constants/games
import {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE
} from '../constants/games';

// GET_GAMES function will be dispatched within GamesContainer
function getGames () {
  return {
    type: GET_GAMES
  };
}

/* After fetching form the server this action is intercepted by the reducer and the games added to the state */
function getGamesSuccess (games) {
  return {
    type: GET_GAMES_SUCCESS,
    games
  };
}

// A failure action is sent in case of server errors
function getGamesFailure () {
  return {
    type: GET_GAMES_FAILURE
  };
}

// we export all the function in a single export command
export {
  getGames,
  getGamesSuccess,
  getGamesFailure
};

The three functions are action creators which returns a plain object description of the action which is going to be executed by the store:

  • getGames will be run in componentDidMount of GamesContainer and requires the HTTP request to the server to fetch the games list. Since we are talking about async requests (the HTTP request), the reducer won't be involved in this, instead we are going to write a specific saga to deal with it.
  • getGamesSuccess and getGamesFailure returns actions digested by the reducer function once the HTTP request terminates as we want to handle both cases, success and failure. Besides, the first one carries the games list received from the server as second property.

Constants

At the top of the file we imported three constants to define the actions type, so let's create that file now. Create the /constants folder in /client/src and create a new file games.js inside of it. Then, paste the following code:

/* the constants are imported in the sagas, reducers and action files so it's very convenient to have a 'centralized' file for them. */
const GET_GAMES = 'GET_GAMES';
const GET_GAMES_SUCCESS = 'GET_GAMES_SUCCESS';
const GET_GAMES_FAILURE = 'GET_GAMES_FAILURE';

export {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE
};

While using constants for the actions type is not necessary I recommend a similar code organization for larger apps.

Reducer

Now we have some actions but we need the reducer function to receive them and return a new state accordingly. Let's create a file games.js in /clients/src/reducers and paste the following code:

import Immutable from 'immutable';
// Here the constants file comes handy
import {
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE
} from '../constants/games';

// The initial state is just an empty Map
const initialState = Immutable.Map();

// That's a very standard reducer function to return a new state given a dispatched action
export default (state = initialState, action) => {
  switch (action.type) {
  // GET_GAMES_SUCCESS case return a new state with the fetched games in the state
    case GET_GAMES_SUCCESS: {
      return state.merge({ list: action.games });
    }
  // In case of failure it simplies returned a new empty state
    case GET_GAMES_FAILURE: {
      return state.clear();
    }
    default:
      return state;
  }
}

Here it is our pure reducer, which receives the current state and an action and returns a new state. In case the state is not passed as parameter, the initialState is considered instead. As a rule, in case the action.type is unhandled (so no case for it), the reducer just the returns the current state.

Now, the entire app must have a single reducer, however we can split the logic in several functions and then combine them all. We created one for now but we can forecast we will have others as well: For instance, the form may have another one!

The important thing to keep in mind is that the reducer function must be one when we create the store, so we need a mechanism to merge them into a single one. Luckily we can use combineReducers from Redux-immutable to achieve this!

NB: We actually need Redux-immutable to have an equivalent function to Redux combineReducers that deals with immutability. If the state wasn't an immutable data-structure we wouldn't need it.

Let's add the package:

yarn add redux-immutable

In /client/src/reducers create index.js and paste the following code:

// We import the combineReducers function
import { combineReducers } from 'redux-immutable';
// Import our reducers function from here
import games from './games'; 

// combineReducers merges them all!
export default combineReducers({
  games
});

For now it is just games but we will soon have others.

Sagas

It is time to handle async requests and to do so we are using Redux-saga:

yarn add redux-saga

Sagas are implemented by generator functions which are transpiled thanks to Babel-polyfill. Let's install it:

yarn add babel-polyfill --dev

And we have to modify the entry of the common config object in webpack.config.js:

/* ...code */
entry: {
    app: ['babel-polyfill', PATHS.src]
},
/* ...code */

Now, let's create games.js in /client/src/sagas and paste the following code:

// Import a saga helper
import {
    takeLatest
} from 'redux-saga';
// Saga effects are usesul to interact with the saga middleware
import {
    put,
    call
} from 'redux-saga/effects';
// As predicted a saga will take care of GET_GAMES actions
import {
  GET_GAMES
} from '../constants/games';
// either one is yielded once the fetch is done
import { getGamesSuccess, getGamesFailure } from '../actions/games';

// We moved the fetch from GamesContainer
const fetchGames = () => {
  return fetch('http://localhost:8080/games', {
    // Set the header content-type to application/json
    headers: new Headers({
      'Content-Type': 'application/json'
    })
  })
  .then(response => response.json())
};

// yield call to fetchGames is in a try catch to control the flow even when the promise rejects
function* getGames () {
  try {
    const games = yield call(fetchGames);
    yield put(getGamesSuccess(games));
  } catch (err) {
    yield put(getGamesFailure());
  }
}

// The watcher saga waits for dispatched GET_GAMES actions
function* watchGetGames () {
  yield takeLatest(GET_GAMES, getGames);
}

// Export the watcher to be run in parallel in sagas/index.js
export { 
    watchGetGames
};
  • We have created a watcher saga watchGetGames which spawns getGames on every action dispatched whose action.type is 'GET_GAMES'. We use takeLatest so it will cancel previous running tasks.
  • getGames is gonna yield call(fetchGames) to the saga middleware and wait to the promise to resolve. This suspends the saga till the promise is resolved. We added a try-catch surrounding the instructions to make sure that in case the promise gets rejected we handle the situation and dispatch an failure action to the reducer.

NB: We could have written const games = yield call(fetchGames) in this way:

const games = yield fetchGames()

However call creates a plain object describing the effect which makes it easy to test. For a deeper discussion spend some time reading the documentation.

In our app we want to start all the sagas at once so we need a function rootSaga to start them all. Let's create index.js in /client/src/sagas and paste the following code:

// Import the watcher we have just created
import {
  watchGetGames
} from './games';

export default function* rootSaga () {
// We start all the sagas in parallel
  yield [
    watchGetGames()
  ];
}

This yields an array with the results of starting all the sagas inside (just one for now).

The final step is to create the Saga middleware and connect it to the redux store but actually we have no store yet.

The App Store

Let's solve this immediately, create store.js in /client/src and paste the following code:

// We import Redux and Redux-saga dependencies
import {
  createStore,
  applyMiddleware
} from 'redux';
import createSagaMiddleware from 'redux-saga';
// this comes from our created files
import rootSaga from './sagas';
import reducer from './reducers';

// The function in charge of creating and returning the store of the app
const configureStore = () => {
  const sagaMiddleware = createSagaMiddleware();
  // The store is created with a reducer parameter and the saga middleware
  const store = createStore(
    reducer,
    applyMiddleware(sagaMiddleware)
  );
  // rootSaga starts all the sagas in parallel
  sagaMiddleware.run(rootSaga);

  return store; // Return the state 
}
export default configureStore;

We defined a function configureStore which does the following:

  • Creates the store passing the reducer and the middleware.
  • Start the sagaMiddleware by calling the run function.
  • Return the state.

Add the Provider Component

At this point we need GamesContainer to access the store and subscribe to it. We can do so thanks to the React-redux component Provider.

First install the package:

yarn add react-redux

Then, in /client/src/routes.js replace the code with the following:

import React from 'react';
// We import Provider
import { Provider } from 'react-redux';
// We need the store to be passed to Provider
import configureStore from './store';
// All the previous dependencies from Part1
import { Router, Route, hashHistory, IndexRoute } from 'react-router';
import { AddGameContainer, GamesContainer } from './containers';
import { Home, Archive, Welcome, About, Contact } from './components';

// Call the configureStore function previously exported
const store = configureStore();

// Provider wraps our root component
const routes = (
{/* We pass the store to the provider */}
  <Provider store={store}>
    <Router history={hashHistory}>
      <Route path="/" component={Home}>
        <IndexRoute component={Welcome} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
      </Route>
      <Route path="/games" component={Archive}>
        <IndexRoute component={GamesContainer} />
        <Route path="add" component={AddGameContainer} />
      </Route>
    </Router>
  </Provider>
);

export default routes;

By wrapping our root component Provider we make the store available to all the components.

As final step GamesContainer is required to dispatch actions and read from the state. React-redux also provides a function connect which creates this connection and allow us to export a smart container instead.

Connected GamesContainer Component

Let's take a look at the updated code for /client/src/containers/GamesContainer.js:

 import React, { Component } from 'react';
 // We import connect from react-redux
import { connect } from 'react-redux';
// bindActionCreators comes handy to wrap action creators in dispatch calls
import { bindActionCreators } from 'redux';
import Immutable from 'immutable';
import { Modal, GamesListManager } from '../components';
// we import the action-creators to be binde with bindActionCreators
import * as gamesActionCreators from '../actions/games';

// We do not export GamesContainer as it is 'almost' a dumb component
class GamesContainer extends Component {
  constructor (props) {
    super();
    // For now we still initialize the state
    this.state = { selectedGame: {}, searchBar: '' };
    this.toggleModal = this.toggleModal.bind(this);
    this.deleteGame = this.deleteGame.bind(this);
    this.setSearchBar = this.setSearchBar.bind(this);
  }

  componentDidMount () {
    this.getGames();
  }

  toggleModal (index) {
    this.setState({ selectedGame: this.state.games[index] });
    $('#game-modal').modal();
  }
// GET_GAMES is now dispatched and intercepted by the saga watcher 
  getGames () {
    this.props.gamesActions.getGames();
  }

  deleteGame (id) {
    fetch(`http://localhost:8080/games/${id}`, {
      headers: new Headers({
        'Content-Type': 'application/json',
      }),
      method: 'DELETE',
    })
    .then(response => response.json())
    .then(response => {
      this.setState({ games: this.state.games.filter(game => game._id !== id) });
      console.log(response.message);
    });
  }

  setSearchBar (event) {
    this.setState({ searchBar: event.target.value.toLowerCase() });
  }

  render () {
    const { selectedGame, searchBar } = this.state;
    const { games  } = this.props;
    console.log(games);
    return (
      <div>
        <Modal game={selectedGame} />
        <GamesListManager
          games={games}
          searchBar={searchBar}
          setSearchBar={this.setSearchBar}
          toggleModal={this.toggleModal}
          deleteGame={this.deleteGame}
        />
      </div>
    );
  }
}

// We can read values from the state thanks to mapStateToProps
function mapStateToProps (state) {
  return { // We get all the games to list in the page
    games: state.getIn(['games', 'list'], Immutable.List()).toJS()
  }
}
// We can dispatch actions to the reducer and sagas
function mapDispatchToProps (dispatch) {
  return {
    gamesActions: bindActionCreators(gamesActionCreators, dispatch)
  };
}
// Finally we export the connected GamesContainer
export default connect(mapStateToProps, mapDispatchToProps)(GamesContainer);
  • mapStateToProps is a function with the state as parameter: It returns an object that gives our container access to the state information as props. In this case the games list will be available through this.props.games.
  • mapDispatchToProps allows our container to dispatch actions. We also need bindActionCreators which makes our action creators wrapped into a dispatch call. Through the gamesActions object GamesContainer can now call getGames action creator.
  • Take a look at the GamesContainer getGames function: This is now just a single line where we call the action creator function. Our saga will intercept the action and fetch data from the server!
  • In the constructor we still initialize the state (for now) but we get the games array from our state so we deleted it from the initialization.

Let's see if it still works..

To run the server:

yarn api

And to run webpack-dev-server:

yarn start

Once we connect to http://localhost:3000 here is the result:

Great it works. We went through all the steps to include redux in our app so now we can apply it wherever it is possible.

GamesContainer SearchBar

The search bar is another good candidate as we save the keyword in the state. Plus, we are not required to use Redux-saga in this case.

As we did before, let's picture our state structure to include the search bar keyword:

games : { 
    list : [
        {//...Game1},
        {//...Game2},
        ...
    ],
    searchBar: ''
}

At the same level as the games list makes sense doesn't it?

Let's edit /client/src/actions/games.js to include the new action creator:

// A new constant SET_SEARCH_BAR
import {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR
} from '../constants/games';

function getGames () {
  return {
    type: GET_GAMES
  };
}

function getGamesSuccess (games) {
  return {
    type: GET_GAMES_SUCCESS,
    games
  };
}

function getGamesFailure () {
  return {
    type: GET_GAMES_FAILURE
  };
}

// setSearchBar action-creator has a payload, the keyword typed by the users
function setSearchBar (keyword) {
  return {
    type: SET_SEARCH_BAR,
    keyword
  };
}

export {
  getGames,
  getGamesSuccess,
  getGamesFailure,
  setSearchBar // We export the new action-creators
};
The new action has type SET_SEARCH_BAR and carries the keyword to filter the games.

As did before, let's create the constant, so edit /client/src/constants/games.js:

const GET_GAMES = 'GET_GAMES';
const GET_GAMES_SUCCESS = 'GET_GAMES_SUCCESS';
const GET_GAMES_FAILURE = 'GET_GAMES_FAILURE';
// The new constant
const SET_SEARCH_BAR = 'SET_SEARCH_BAR';

export {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR // We export it too
};

Our reducer switch requires a new case. Let's edit /client/src/reducers/games.js:

import Immutable from 'immutable';
import {
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR
} from '../constants/games';

const initialState = Immutable.Map();

export default (state = initialState, action) => {
  switch (action.type) {
    case GET_GAMES_SUCCESS: {
      return state.merge({ list: action.games });
    }
    // The reducer can now set the searchBar content into the state
    case SET_SEARCH_BAR: {
      return state.merge({ searchBar: action.keyword });
    }
    case GET_GAMES_FAILURE: {
      return state.clear();
    }
    default:
      return state;
  }
}

Again, we merge the state with the current searchBar content.

Finally, it's time to edit GamesContainer:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Immutable from 'immutable';
import { Modal, GamesListManager } from '../components';
import * as gamesActionCreators from '../actions/games';

class GamesContainer extends Component {
  constructor (props) {
    super(props);
   // We removed the searchBar initialization
    this.state = { selectedGame: {} };
    this.toggleModal = this.toggleModal.bind(this);
    this.deleteGame = this.deleteGame.bind(this);
    this.setSearchBar = this.setSearchBar.bind(this);
  }

  componentDidMount () {
    this.getGames();
  }

  toggleModal (index) {
    this.setState({ selectedGame: this.state.games[index] });
    $('#game-modal').modal();
  }

  getGames () {
    this.props.gamesActions.getGames();
  }

  deleteGame (id) {
    fetch(`http://localhost:8080/games/${id}`, {
      headers: new Headers({
        'Content-Type': 'application/json',
      }),
      method: 'DELETE',
    })
    .then(response => response.json())
    .then(response => {
      this.setState({ games: this.state.games.filter(game => game._id !== id) });
      console.log(response.message);
    });
  }
// It now dispatches the action and pass the search bar content as parameter
  setSearchBar (event) {
    this.props.gamesActions.setSearchBar(event.target.value.toLowerCase());
  }

  render () {
  {// we take games and searchBar from props now}
    const { selectedGame } = this.state;
    const { games, searchBar } = this.props;
    console.log(games);
    return (
      <div>
        <Modal game={selectedGame} />
        <GamesListManager
          games={games}
          searchBar={searchBar}
          setSearchBar={this.setSearchBar}
          toggleModal={this.toggleModal}
          deleteGame={this.deleteGame}
        />
      </div>
    );
  }
}

function mapStateToProps (state) {
  return {
    games: state.getIn(['games', 'list'], Immutable.List()).toJS(),
    searchBar: state.getIn(['games', 'searchBar'], '') // We retrieve the searchBar content too
  }
}

function mapDispatchToProps (dispatch) {
  return {
    // setSearchBar gets binded too
    gamesActions: bindActionCreators(gamesActionCreators, dispatch)
  };
}
export default connect(mapStateToProps, mapDispatchToProps)(GamesContainer);
  • in mapStateToProps we retrieve the current value of the search bar from the state which is now accessible within the component at this.props.searchBar.
  • mapDispatchToProps doesn't change as it's already an object whose properties are the exported action creators.
  • The GamesContainer setSearchBar function now dispatches the action to the reducer passing the current search bar value.
  • In the constructor we removed the initiliaziation of the keyword.

Let's take a look at the result in the browser:

If you try to click "view" on any game you receive an error, we need to modify our modal behavior! The process is very similar to the search bar one.

GamesContainer Modal

This is our state including the selectedGame:

games : { 
    list : [
        {//...Game1},
        {//...Game2},
        ...
    ],
    searchBar: '',
    selectedGame: { //... Game to show in the modal }
}

In client/src/actions/games.js let's define an action creator:

import {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR,
  SHOW_SELECTED_GAME // Another constant
} from '../constants/games';

function getGames () {
  return {
    type: GET_GAMES
  };
}

function getGamesSuccess (games) {
  return {
    type: GET_GAMES_SUCCESS,
    games
  };
}

function getGamesFailure () {
  return {
    type: GET_GAMES_FAILURE
  };
}

function setSearchBar (keyword) {
  return {
    type: SET_SEARCH_BAR,
    keyword
  };
}

// We pass the game as payload
function showSelectedGame (game) {
  return {
    type: SHOW_SELECTED_GAME,
    game
  };
}

export {
  getGames,
  getGamesSuccess,
  getGamesFailure,
  setSearchBar,
  showSelectedGame // Export the new action-creator
};

We also must define a new constant SHOW_SELECTED_GAME.

Edit /client/src/constants/games.js:

const GET_GAMES = 'GET_GAMES';
const GET_GAMES_SUCCESS = 'GET_GAMES_SUCCESS';
const GET_GAMES_FAILURE = 'GET_GAMES_FAILURE';
const SET_SEARCH_BAR = 'SET_SEARCH_BAR';
// Define the latest constant
const SHOW_SELECTED_GAME = 'SHOW_SELECTED_GAME';

export {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR,
  SHOW_SELECTED_GAME // Export the new constant
};

Again, let's add the case in the games reducer /client/src/reducers/games.js:

import Immutable from 'immutable';
import {
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR,
  // Import the new constant to be used as new 'case'
  SHOW_SELECTED_GAME
} from '../constants/games';

const initialState = Immutable.Map();

export default (state = initialState, action) => {
  switch (action.type) {
    case GET_GAMES_SUCCESS: {
      return state.merge({ list: action.games });
    }
    case SET_SEARCH_BAR: {
      return state.merge({ searchBar: action.keyword });
    }
   // We finally moved the selectedGame in the app state
    case SHOW_SELECTED_GAME: {
      return state.merge({ selectedGame: action.game });
    }
    case GET_GAMES_FAILURE: {
      return state.clear();
    }
    default:
      return state;
  }
}

We can finally edit our GamesContainer:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Immutable from 'immutable';
import { Modal, GamesListManager } from '../components';
import * as gamesActionCreators from '../actions/games';

// GamesContainer does not initialize the state anymore
class GamesContainer extends Component {
  constructor (props) {
    super(props);
    this.toggleModal = this.toggleModal.bind(this);
    this.deleteGame = this.deleteGame.bind(this);
    this.setSearchBar = this.setSearchBar.bind(this);
  }

  componentDidMount () {
    this.getGames();
  }
// Once the action is dispatched we toggle the modal
  toggleModal (index) {
  // We pass the game given the index parameter passed from the view button
    this.props.gamesActions.showSelectedGame(this.props.games[index]);
    $('#game-modal').modal();
  }

  getGames () {
    this.props.gamesActions.getGames();
  }

  deleteGame (id) {
    fetch(`http://localhost:8080/games/${id}`, {
      headers: new Headers({
        'Content-Type': 'application/json',
      }),
      method: 'DELETE',
    })
    .then(response => response.json())
    .then(response => {
      this.setState({ games: this.state.games.filter(game => game._id !== id) });
      console.log(response.message);
    });
  }

  setSearchBar (event) {
    this.props.gamesActions.setSearchBar(event.target.value.toLowerCase());
  }

  render () {
   {/* We get all the info from props */}
    const { games, selectedGame, searchBar } = this.props;
    return (
      <div>
        <Modal game={selectedGame} />
        <GamesListManager
          games={games}
          searchBar={searchBar}
          setSearchBar={this.setSearchBar}
          toggleModal={this.toggleModal}
          deleteGame={this.deleteGame}
        />
      </div>
    );
  }
}

function mapStateToProps (state) {
  return {
    games: state.getIn(['games', 'list'], Immutable.List()).toJS(),
    searchBar: state.getIn(['games', 'searchBar'], ''),
    // The latest addition to props is the selectedGame
    selectedGame: state.getIn(['games', 'selectedGame'], Immutable.List()).toJS() 
  }
}

function mapDispatchToProps (dispatch) {
  return {
    gamesActions: bindActionCreators(gamesActionCreators, dispatch)
  };
}
export default connect(mapStateToProps, mapDispatchToProps)(GamesContainer);
  • We now map selectedGame from the state to the props, so it's available at this.props.selectedGame.
  • We can dispatch the new action through gamesActions props. You take a look at the function toggleModal: It dispatches the new action with the selected game and toggle the modal.

At this point check the app in the browser:

That's awesome because we already achieve a big result: GamesContainer is now a dumb component as it has no state! Its connected version instead is a smart component because connected to the Redux store.

We are almost done, we just need to rewrite the logic to delete a game.

Delete a Game

Let's start from the actions, since we are gonna write another HTTP request we can make assumptions based on the getGames logic: Inside a try catch we send a DELETE request to the server and if everything goes well the next action to the reducer will be DELETE_GAME_SUCCESSFUL, otherwise the catch block will send DELETE_GAME_FAILURE.

So let's edit /client/src/actions/games.js:

import {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR,
  SHOW_SELECTED_GAME,
  // We import the three constants
  DELETE_GAME,
  DELETE_GAME_SUCCESS,
  DELETE_GAME_FAILURE
} from '../constants/games';

function getGames () {
  return {
    type: GET_GAMES
  };
}

function getGamesSuccess (games) {
  return {
    type: GET_GAMES_SUCCESS,
    games
  };
}

function getGamesFailure () {
  return {
    type: GET_GAMES_FAILURE
  };
}

function setSearchBar (keyword) {
  return {
    type: SET_SEARCH_BAR,
    keyword
  };
}

function showSelectedGame (game) {
  return {
    type: SHOW_SELECTED_GAME,
    game
  };
}

// This is called when a user clicks on the delete button
function deleteGame () {
  return {
    type: DELETE_GAME
  };
}
// In case of succesful deletion the action is dispatched to the reducer
function deleteGamesSuccess (games) {
  return {
    type: DELETE_GAME_SUCCESS,
    games
  };
}
// In case of failure the saga dispatches DELETE_GAME_FAILURE instead
function deleteGameFailure () {
  return {
    type: DELETE_GAME_FAILURE
  };
}

export {
  getGames,
  getGamesSuccess,
  getGamesFailure,
  setSearchBar,
  showSelectedGame,
  // Export the 3 new functions
  deleteGame,
  deleteGameSuccess,
  deleteGameFailure
};
  • deleteGame returns the action a new saga takes, it will be run from the GameContainer and has the game id as parameter.
  • The remaining go from the saga to the reducer according to the HTTP request result. In particular, deleteGameSuccess carries the games... Why? That's because once the game is deleted we filter the current games list from the state and delete it from the list as well. Then the reducer will merge the new games list and return a new state. This is the same as what GET_GAMES_SUCCESS does!

We need to edit the constants file as well, open /client/src/constants and paste the following code:

const GET_GAMES = 'GET_GAMES';
const GET_GAMES_SUCCESS = 'GET_GAMES_SUCCESS';
const GET_GAMES_FAILURE = 'GET_GAMES_FAILURE';
const SET_SEARCH_BAR = 'SET_SEARCH_BAR';
const SHOW_SELECTED_GAME = 'SHOW_SELECTED_GAME';
// Here's the definition for the new 3 constants
const DELETE_GAME = 'DELETE_GAME';
const DELETE_GAME_SUCCESS = 'DELETE_GAME_SUCCESS';
const DELETE_GAME_FAILURE = 'DELETE_GAME_FAILURE';

export {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR,
  SHOW_SELECTED_GAME,
  // Export the new constants
  DELETE_GAME,
  DELETE_GAME_SUCCESS,
  DELETE_GAME_FAILURE
};

Now let's create a new saga so let's edit /client/src/sagas/games.js:

import {
  takeLatest,
  delay
} from 'redux-saga';
import {
  put,
  select,
  call
} from 'redux-saga/effects';
// We import DELETE_GAME constant for the new saga watcher
import {
  GET_GAMES,
  DELETE_GAME
} from '../constants/games';
import {
    getGamesSuccess,
    getGamesFailure ,
    // the last two action creators are imported as well
    deleteGameSuccess,
    deleteGameFailure
} from '../actions/games';

// Selector function to return the games list from the state
const selectedGames = (state) => {
  return state.getIn(['games', 'list']).toJS();
}

const fetchGames = () => {
  return fetch('http://localhost:8080/games', {
    headers: new Headers({
      'Content-Type': 'application/json'
    })
  })
  .then(response => response.json());
};

const deleteServerGame = (id) => {
  return fetch(`http://localhost:8080/games/${id}`, {
    headers: new Headers({
      'Content-Type': 'application/json',
    }),
    method: 'DELETE',
  })
  .then(response => response.json());
}

function* getGames () {
  try {
    const games = yield call(fetchGames);
    yield put(getGamesSuccess(games));
  } catch (err) {
    yield put(getGamesFailure());
  }
}

function* deleteGame (action) {
  const { id } = action;
  // We take the games from the state
  const games = yield select(selectedGames);
  try {
    yield call(deleteServerGame, id);
    // The new state will contain the games except for the deleted one.
    yield put(deleteGameSuccess(games.filter(game => game._id !== id)));
  } catch (e) {
    // In case of error 
    yield put(deleteGameFailure());
  }
}

function* watchGetGames () {
  yield takeLatest(GET_GAMES, getGames);
}
// The new watcher intercepts the action and run deleteGame
function* watchDeleteGame () {
    yield takeLatest(DELETE_GAME, deleteGame);
}

export {
    watchGetGames,
    watchDeleteGame
};
  • We created the watchDeleteGame saga in charge to intercept the action DELETE_GAME.
  • In deleteGame we first take advantage of the effect select form Redux-saga to retrieve information from the state: the function needs the games list because if everything goes well, it will the deleted game from it and send it along with the action DELETE_GAME_SUCCESS. As I mentioned before, the filter function from javascript array comes handy, we can easily build a new games list without the deleted game and pass it as parameter to deleteGameSuccess.

We also need to edit /client/src/sagas/index.js to run watchDeleteGame in parallel with watchGetGames:

import {
  watchGetGames,
  watchDeleteGame
} from './games';

export default function* rootSaga () {
  yield [
    watchGetGames(),
    watchDeleteGame() // must be run in parallel
  ];
}

We almost finished, let's edit the reducer games.js:

import Immutable from 'immutable';
import {
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR,
  SHOW_SELECTED_GAME,
  DELETE_GAME_SUCCESS,
  DELETE_GAME_FAILURE
} from '../constants/games';

const initialState = Immutable.Map();

export default (state = initialState, action) => {
  switch (action.type) {
  // Both cases share the same behavior in fact
    case DELETE_GAME_SUCCESS:
    case GET_GAMES_SUCCESS: {
      return state.merge({ list: action.games });
    }
    case SET_SEARCH_BAR: {
      return state.merge({ searchBar: action.keyword });
    }
    case SHOW_SELECTED_GAME: {
      return state.merge({ selectedGame: action.game });
    }
    // We can simply assume all the failures clear the state
    case DELETE_GAME_FAILURE:
    case GET_GAMES_FAILURE: {
      return state.clear();
    }
    default:
      return state;
  }
}
  • As explained before, the action DELETE_GAME_SUCCESS does the same as GET_GAMES_SUCCESS so the two cases can do the same as well.
  • Could be a better idea to separate the behavior of DELETE_GAME_FAILURE and GET_GAMES_FAILURE, however for the purpose of the tutorial we can just assume that whenever the server is down we simply return a new state with empty values.

Finally, we need to dispatch the action DELETE_GAME within our container, let's edit /client/src/containers/GamesContainer.js:


// ...Code
  deleteGame (id) { // It simplies dispatches the action including the game id
    this.props.gamesActions.deleteGame(id);
  }
// ...Code

It's not necessary to show the entire code, we just need to modify the deleteGame function to dispatch the action.

Easy as pie!

Let's try to delete a game in the browser, just go to http://localhost:3000:

Rewrite AddGameContainer

We are almost done but we have to rewrite the AddGameContainer and Form to use Redux.

Form Component

If you take a look at the code of AddGameContainer you can immediately figure out what to do:

  • We need a new action to post the game to the server and dispatch it from its function uploadPicture. The procedure involves moving the server POST request in a saga and perhaps dispatch another action to the reducer.
  • However, setGame touches the app state as it keeps track of the user input while adding the new game. We can easily get rid of it by using Redux-form.
  • What about uploadPicture then? We can move our picture uploader into a saga function too and keep the url in the state.

NB: We are going to touch just the surface of Redux-form, I do suggest you to take a look at its guide for more.

Let's start by adding Redux-form to our dependencies:

yarn add redux-form --dev

Then take a look at the state new structure:

games : { 
    list : [
        {//...Game1},
        {//...Game2},
        ...
    ],
    searchBar: '',
    selectedGame: { //... Game to show in the modal }
},
form : {
    game: {//... it will contain several pieces of information as well as the inputs value}
},
filestack : {
    url : 'picture_url' //... Trivial, this is where we 'save' the picture url
}
  • game is the name of our form specified when the component is decorated by reduxForm.
  • On the other hand the picture url is available at filestack.url

And as first thing let's rewrite the Form component, edit /client/src/components/Form.js with the following code:

import React, { PureComponent } from 'react';
import { Link } from 'react-router';
// We import Field and reduxForm from redux-form immutable version
import { Field, reduxForm } from 'redux-form/immutable';

class Form extends PureComponent {
  render () {
    const { picture, uploadPicture } = this.props;
    return (
      <div className="row scrollable">
          <div className="col-md-offset-2 col-md-8">
        <div className="text-left">
            <Link to="/games" className="btn btn-info">Back</Link>
        </div>
        <div className="panel panel-default">
        <div className="panel-heading">
            <h2 className="panel-title text-center">
                 Add a Game!
            </h2>
        </div>
        <div className="panel-body">
        <form onSubmit={this.props.handleSubmit}>
                <div className="form-group text-left">
                  <label htmlFor="name">Name</label>
    {/* All the previous form input become Field components. 
    Notice that Field render the right form input given the value of component */}
                  <Field 
                    name="name" 
                    type="text" 
                    className="form-control" 
                    component="input" 
                    placeholder="Enter the name" 
                  />
                </div>
                <div className="form-group text-left">
                  <label htmlFor="description">Description</label>
    {/* The description textarea becomes a Field component too */}
                  <Field 
                    name="description" 
                    component="textarea" 
                    className="form-control" 
                    placeholder="Enter the description" 
                    rows="5" 
                  />
                </div>
                <div className="form-group text-left">
                  <label htmlFor="price">Year</label>
    {/* ... And the input number for the year */}
                  <Field 
                    name="year" 
                    component="input" 
                    type="number" 
                    className="form-control" 
                    placeholder="Enter the year" 
                  />
                </div>
          <div className="form-group text-left">
               <label htmlFor="picture">Picture</label>
               <div className="text-center dropup">
            <button 
              id="button-upload" 
              type="button" 
              className="btn btn-danger" 
              onClick={() => uploadPicture()}
            >
              Upload <span className="caret" />
            </button>
              </div>
            </div>
            <div className="form-group text-center">
            <img id="picture" className="img-responsive img-upload" src={picture} />
            </div>
            <button type="submit" className="btn btn-submit btn-block">Submit</button>
        </form>
         </div>
       </div>
    </div>
</div>
    );
  }
}

// we named the form game so that in the state we can access it like form.game
export default reduxForm({ form: 'game' })(Form);
  • We included from Field and reduxForm: The first is a component to connect a field to the redux store while the second is also a component but it wraps the Form component in a high order component instead. Once we add the form reducer our state will keep up-to-date with our Field inputs as it listens to actions dispatched from reduxForm.
  • Also, they are both the immutable version (redux-form/immutable) as our state is an immutable data-structure.

Form Reducer

As last step let's add the form reducer, edit the /client/src/reducer/index.js and paste the following code:

import { combineReducers } from 'redux-immutable';
// Even here we need to include the immutable version
import { reducer as form } from 'redux-form/immutable';
import games from './games';

// Now you can see the benefit of using combineReducers!
export default combineReducers({
  games,
  form
});

If you now try to play with the app and type anything in the form fields, Redux-form will automatically dispatch special actions to keep the game info in the state. Plus, and this is great, if you go back to the games list the form will automatically remove its information from the state as well.

Still, we can't actually create any object yet, we need the sagas for it, as well as for uploading the picture on Filestack.

These are the next steps.

The Actions

We can think of the actions for both sagas the same way we did for the previous ones: We have an action dispatched from the component/container and two actions, one for success and one for failure, both yielded by the saga.

First, let's write the actions for adding a new game, so edit /client/src/actions/games.js and paste the following code:

import {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR,
  SHOW_SELECTED_GAME,
  DELETE_GAME,
  DELETE_GAME_SUCCESS,
  DELETE_GAME_FAILURE,
  // Import new constants
  POST_GAME,
  POST_GAME_SUCCESS,
  POST_GAME_FAILURE
} from '../constants/games';

function getGames () {
  return {
    type: GET_GAMES
  };
}

function getGamesSuccess (games) {
  return {
    type: GET_GAMES_SUCCESS,
    games
  };
}

function getGamesFailure () {
  return {
    type: GET_GAMES_FAILURE
  };
}

function setSearchBar (keyword) {
  return {
    type: SET_SEARCH_BAR,
    keyword
  };
}

function showSelectedGame (game) {
  return {
    type: SHOW_SELECTED_GAME,
    game
  };
}

function deleteGame (id) {
  return {
    type: DELETE_GAME,
    id
  };
}

function deleteGameSuccess (games) {
  return {
    type: DELETE_GAME_SUCCESS,
    games
  };
}

function deleteGameFailure () {
  return {
    type: DELETE_GAME_FAILURE
  };
}

// POST_GAME is dispatched when users click on submit
function postGame () {
  return {
    type: POST_GAME
  };
}

// The action is dispatched when the returned promise from a POST request resolve
function postGameSuccess () {
  return {
    type: POST_GAME_SUCCESS
  };
}

// In case of failure
function postGameFailure () {
  return {
    type: POST_GAME_FAILURE
  };
}

export {
  getGames,
  getGamesSuccess,
  getGamesFailure,
  setSearchBar,
  showSelectedGame,
  deleteGame,
  deleteGameSuccess,
  deleteGameFailure,
  // Export the new action-creators
  postGame,
  postGameSuccess,
  postGameFailure
};

Notice that POST_GAME doesn't carry any payload, the saga takes it directly from the state. Next, we create a new file filestack.js in /client/src/actions and paste the following code:

// Import constants (obviously)
import {
  UPLOAD_PICTURE,
  UPLOAD_PICTURE_SUCCESS,
  UPLOAD_PICTURE_FAILURE
} from '../constants/filestack';

// Triggered by the upload button
function uploadPicture () {
  return {
    type: UPLOAD_PICTURE
  };
}

// It carries the picture url to be added to the state
function uploadPictureSuccess (url) {
  return {
    type: UPLOAD_PICTURE_SUCCESS,
    url
  };
}

// In case of failure
function uploadPictureFailure () {
  return {
    type: UPLOAD_PICTURE_FAILURE
  };
}

export {
  uploadPicture,
  uploadPictureSuccess,
  uploadPictureFailure
};

Nothing exotic here, UPLOAD_PICTURE_SUCCESS has a payload which is the CDN url returned by Filestack.

Again, we are adding functionalities to the app while following a similar pattern. Right after the actions creators we need to define the new constants used for the action.type property. Open /client/src/constants/games.js and paste the following code:

const GET_GAMES = 'GET_GAMES';
const GET_GAMES_SUCCESS = 'GET_GAMES_SUCCESS';
const GET_GAMES_FAILURE = 'GET_GAMES_FAILURE';
const SET_SEARCH_BAR = 'SET_SEARCH_BAR';
const SHOW_SELECTED_GAME = 'SHOW_SELECTED_GAME';
const DELETE_GAME = 'DELETE_GAME';
const DELETE_GAME_SUCCESS = 'DELETE_GAME_SUCCESS';
const DELETE_GAME_FAILURE = 'DELETE_GAME_FAILURE';
// The new constants definition
const POST_GAME = 'POST_GAME';
const POST_GAME_SUCCESS = 'POST_GAME_SUCCESS';
const POST_GAME_FAILURE = 'POST_GAME_FAILURE';

export {
  GET_GAMES,
  GET_GAMES_SUCCESS,
  GET_GAMES_FAILURE,
  SET_SEARCH_BAR,
  SHOW_SELECTED_GAME,
  DELETE_GAME,
  DELETE_GAME_SUCCESS,
  DELETE_GAME_FAILURE,
  // Export the constants
  POST_GAME,
  POST_GAME_SUCCESS,
  POST_GAME_FAILURE
};

Then create a new constants file called filestack.js and paste the following code:

// A very simple file but we want to keep the constants for filestack separated to another file
const UPLOAD_PICTURE = 'UPLOAD_PICTURE';
const UPLOAD_PICTURE_SUCCESS = 'UPLOAD_PICTURE_SUCCESS';
const UPLOAD_PICTURE_FAILURE = 'UPLOAD_PICTURE_FAILURE';

export {
  UPLOAD_PICTURE,
  UPLOAD_PICTURE_SUCCESS,
  UPLOAD_PICTURE_FAILURE
};

Filestack Reducer

We obviously need a new reducer for Filestack related actions, let's create filestack.js in /client/src/reducers and paste the following code:

import Immutable from 'immutable';
// import the constants
import {
  UPLOAD_PICTURE_SUCCESS,
  UPLOAD_PICTURE_FAILURE
} from '../constants/filestack';
// Also import the constants for the post game actions
import {
  POST_GAME_SUCCESS,
  POST_GAME_FAILURE
} from '../constants/games';

// The initial state is just a Map
const initialState = Immutable.Map();

export default (state = initialState, action) => {
  switch (action.type) {
  // The url is saved in filestack.url
    case UPLOAD_PICTURE_SUCCESS: {
      return state.merge({ url: action.url });
    }
   // After a game was posted we want to clear the state from the picture url as well
    case POST_GAME_SUCCESS:
    case POST_GAME_FAILURE:
    case UPLOAD_PICTURE_FAILURE: {
      return state.clear();
    }
    default:
      return state;
  }
}

Notice that also the actions after the game submission are intercepted by the reducer: We want to clear the state which means delete the picture url from it. As said before, once we submit we change the view with hashHistory, so the Redux-form game will be automatically deleted from the state while filestack.url will persist.

Let's now combine it with the others in /client/src/reducers/index.js:

import { combineReducers } from 'redux-immutable';
import { reducer as form } from 'redux-form/immutable';
import games from './games';
import filestack from './filestack';

export default combineReducers({
  games,
  form,
  filestack // Include the filestack reducer to be combined into a single one
});

AddGameContainer Sagas

Now let's talk about sagas, we need to write a few, let's start from adding the game: Open /client/src/sagas/games.js and paste the following code:

import { takeLatest } from 'redux-saga';
import {
    put,
    select,
    call
} from 'redux-saga/effects';
import {
  GET_GAMES,
  DELETE_GAME,
  POST_GAME // import the constant to be used by the watcher
} from '../constants/games';
import {
  getGamesSuccess,
  getGamesFailure ,
  deleteGameSuccess,
  deleteGameFailure,
  // Import the action creators to handle the server POST request outcome
  postGameSuccess,
  postGameFailure
} from '../actions/games';

const selectedGames = (state) => {
  return state.getIn(['games', 'list']).toJS();
}

// selector to get the picture from the state
const selectedPicture = (state) => {
  return state.getIn(['filestack', 'url'], '');
}

const fetchGames = () => {
  return fetch('http://localhost:8080/games', {
    headers: new Headers({
      'Content-Type': 'application/json'
    })
  })
  .then(response => response.json());
};

const deleteServerGame = (id) => {
  return fetch(`http://localhost:8080/games/${id}`, {
    headers: new Headers({
      'Content-Type': 'application/json',
    }),
    method: 'DELETE',
  })
  .then(response => response.json());
}

// the function contains the fetch logic to add a game
const postServerGame = (game) => {
  return fetch('http://localhost:8080/games', {
    headers: new Headers({
      'Content-Type': 'application/json'
    }),
    method: 'POST',
    body: JSON.stringify(game)
  })
  .then(response => response.json());
}

function* getGames () {
  try {
    const games = yield call(fetchGames);
    yield put(getGamesSuccess(games));
  } catch (err) {
    yield put(getGamesFailure());
  }
}

function* deleteGame (action) {
  const { id } = action;
  const games = yield select(selectedGames);
  try {
    yield call(deleteServerGame, id); // API call
    yield put(deleteGameSuccess(games.filter(game => game._id !== id)));
  } catch (e) {
    // In case of error
    yield put(deleteGameFailure());
  }
}

const getGameForm = (state) => {
  return state.getIn(['form', 'game']).toJS();
}

function* postGame () {
  // Access the state to retrieve the new game information
  const picture = yield select(selectedPicture);
  const game = yield select(getGameForm);
  // Create the newGame object to be sent to the server
  const newGame = Object.assign({}, { picture }, game.values);
  try {
    // yield call postServerGame to post to the server
    yield call(postServerGame, newGame);
    yield put(postGameSuccess());
  } catch (e) {
    yield put(postGameFailure());
  }
}

function* watchGetGames () {
  yield takeLatest(GET_GAMES, getGames);
}

function* watchDeleteGame () {
    yield takeLatest(DELETE_GAME, deleteGame);
}

// The new watcher saga to intercept POST_GAME actions
function* watchPostGame () {
  yield takeLatest(POST_GAME, postGame);
}

export {
    watchGetGames,
    watchDeleteGame,
    watchPostGame // Export the new watcher to be run in parallel
};

The postGame function by yielding select twice with a selector function as parameter is able to get the games information and picture. Then, we run the fetch function and post to the game to the server.

Regarding Filestack, we have to rethink about the pick function: while the first parameter is an object of options the others are all function, we have onSuccess, onFailure and in fact onProgress too (to learn more about it just take a look a the documentation). Unfortunately pick doesn't not return any promise but sagas requires that, so we can take advantage of the onSuccess and onFailure function parameters to resolve or reject a promise.

At Filestack they tried their best to provide very flexible functions that users can customize for their needs, this is a perfect example.

Let's create filestack.js in /client/src/sagas and paste the following code:

import { takeLatest } from 'redux-saga';
import { put, call } from 'redux-saga/effects';
import { UPLOAD_PICTURE } from '../constants/filestack';
import {
  uploadPictureSuccess,
  uploadPictureFailure
} from '../actions/filestack';

const pick = () => {
   return new Promise((resolve, reject) => {
    filepicker.pick (
      {
      // The options are the same as in part1
        mimetype: 'image/*',
        container: 'modal',
        services: ['COMPUTER', 'FACEBOOK', 'INSTAGRAM', 'URL', 'IMGUR', 'PICASA'],
        openTo: 'COMPUTER'
      },
      function (Blob) {
        console.log(JSON.stringify(Blob));
        const handler = Blob.url;
        resolve(handler); // The promise resolves
      },
      function (FPError) {
        console.log(FPError.toString());
        reject(FPError.toString()); // the promise rejects
      }
    );
  });
}

function* uploadPicture () {
  try {
    const url = yield call(pick); // call the pick function
    yield put(uploadPictureSuccess(url));
  } catch (error) {
    yield put(uploadPictureFailure());
  }
}

export function* watchUploadPicture () {
  yield takeLatest(UPLOAD_PICTURE, uploadPicture);
}

The function pick yielded by uploadPicture return a promise which either resolves in onSuccess or rejects in onFailure.

Let's update /client/src/sagas/index.js to run the new sagas:

import {
  watchGetGames,
  watchDeleteGame,
  watchPostGame
} from './games';
import { watchUploadPicture } from './filestack';

export default function* rootSaga () {
  yield [
    watchGetGames(),
    watchDeleteGame(),
    watchPostGame(),
    watchUploadPicture() // Run the last saga in parallel with the others
  ];
}

The last thing we need to do is to edit addGameContainer:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { hashHistory } from 'react-router';
import { Form } from '../components';
import * as gamesActionCreators from '../actions/games';
import * as filestackActionCreators from '../actions/filestack';

class AddGameContainer extends Component {
  constructor (props) {
    super(props);
    this.submit = this.submit.bind(this);
    this.uploadPicture = this.uploadPicture.bind(this);
  }
  // Dispatch POST_GAME to the saga and change the view
  submit (event) {
    event.preventDefault();
    this.props.gamesActions.postGame();
    hashHistory.push('/games');
  }
  // Dispatch UPLOAD_PICTURE to the filestack saga
  uploadPicture () {
    this.props.filestackActions.uploadPicture();
  }
  render () {
    const { picture } = this.props;
    return (
      <Form
        handleSubmit={this.submit}
        picture={picture}
        uploadPicture={this.uploadPicture}
      />
    );
  }
}

function mapStateToProps (state) {
  return {
  // We access the state to retrieve the url and show the preview of the image in the form
    picture: state.getIn(['filestack', 'url'], '')
  }
}

function mapDispatchToProps (dispatch) {
  return {
  // We get the actions to dispatch POST_GAME actions and UPLOAD_PICTURE too
    gamesActions: bindActionCreators(gamesActionCreators, dispatch),
    filestackActions: bindActionCreators(filestackActionCreators, dispatch)
  };
}
export default connect(mapStateToProps, mapDispatchToProps)(AddGameContainer);

Now try to add a game in http://localhost:3000, or first run yarn build and serve the page from Node.js at http://localhost:8080!

Conclusions

In this second part of the tutorial we defined a single state and decoupled from the containers/components logic.

To do so we use Redux so that we have a reducer to intercept actions and always provide a new state to the app. We also included Redux-saga to control all the async behavior of our app (HTTP requests and Filestack uploader).

To facilitate this process we covered each step required to integrate Redux: We started to define actions, reducers and sagas to intercept them, created the store and connected to react through Provider component. Finally, we exported connected versions of the containers which are able to read from the state and dispatch actions.

Stay tuned for the third part, the bonus part where we will improve the UI and add basic authentication!

PS: The store I created in my github I compose the saga middleware with redux-dev-tools middleware. You can download the browser extension and checkout the state, all the dispatched actions and much more!

Samuele Zaza

6 posts

I am a full-stack web developer working for Taroko Software as front-end web developer and Filestack Tech Evangelist. When not coding I may be spotted in a gym lifting or planning to conquer the world LOL.