Contents


Manage state with Redux, Part 3

Implementing asynchronous actions with Redux

Compose reducers and implement asynchronous actions

Comments

Content series:

This content is part # of 5 in the series: Manage state with Redux, Part 3

Stay tuned for additional content in this series.

This content is part of the series:Manage state with Redux, Part 3

Stay tuned for additional content in this series.

In Part 2 of this series, "Using Redux with React," you explored use of the react-redux bindings to make the Redux store available to all of the React components in a React application's component hierarchy. You also saw how to implement action creators, and how to separate container components that automatically connect to the Redux store from presentation components that display information based on the current state.

In this third installment, I continue building the book-search application, which uses Redux for state management. The application code is React-specific, but regardless of your application framework or lack thereof, you'll learn how to use three additional advanced aspects of Redux:

  • Composing reducers
  • Implementing asynchronous actions
  • Using and implementing Redux middleware

The book-search app in review

With the book-search application, shown in Figure 1, you search for books by topic via the Google Books search API. Each search results in 10 or fewer books for the current topic, and the cover of each book is displayed. The cover thumbnails are links that take you to book details.

Figure 1. The book-search application

What's of interest isn't the search results, but how the application maintains state. Perhaps even more interesting is the app's history component, with which users can move back and forth through the application's state. All the while, the application displays the current state at the bottom of the page.

Recall from Part 2 that the app's starting point looks like Figure 2.

Figure 2. The starting point for the book-search app

Listing 1 (identical to Listing 7 in Part 2 and repeated here for convenience) shows the application's entry-point code.

Listing 1. The entry point (index.js)
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import App from './containers/app';
import store from './store';
import { setTopic, setDisplayMode } from './actions';

store.dispatch(setTopic('javascript'));
store.dispatch(setDisplayMode('THUMBNAIL'));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('example')
)

function showState() {
  const state = store.getState();
}

store.subscribe(showState);

The application dispatches two actions — created by the setTopic() and setDisplayMode() action-creator functions — to set the initial state to javascript for the topic and THUMBNAIL for the display mode. (For a refresher on the setTopic() and setDisplayMode() action creators, see "Using Redux with React.")

The Provider component — part of the react-redux bindings — makes all of its properties available to any components inside the Provider tag body — in this case, a single property: the Redux store.

As you can see in Listing 1, the application's entry point imports the store, specifying it as a property of the Provider component. The Redux store is created and exported in store.js, which consists in its entirety of:

import { createStore } from 'redux';
import reducers from './reducers';

export const store = createStore(reducers);

The store.js code creates the Redux store by calling Redux.createStore(). The application passes the requisite reducer to createStore().

Notice that the reducer exported from reducers.js is named reducers with an s, instead of reducer— because that reducer is actually a combination of reducers.

Combining reducers

Ultimately, the state for the finished book-search application will contain the following information:

  • A topic
  • A list of books
  • The current fetch status (started, completed, or failed)
  • The display mode (thumbnail or list)

The application displays a list of books for a particular topic. Because the application fetches books asynchronously, the application tracks the status of the current fetch. The app can display books as thumbnails of their covers, as shown in Figure 1, or as a textual list, so the application also tracks the display mode: list or thumbnail.

Recall that state in a Redux application is stored in a single JavaScript object. The state for the final book-search application will have four properties corresponding to the four pieces of information in the preceding list:

  • topic
  • books
  • currentStatus
  • displayMode

Also recall that Redux applications, besides having exactly one object for state, have exactly one reducer for that state. However, for nontrivial applications, it quickly becomes unwieldy to maintain a single reducer function that creates application state. The code would be more readable, maintainable, and extensible if it had four reducers, one for each piece of state. The good news is that you can use the combineReducers() function to combine any number of reducers into a single reducer.

Figure 3 shows the nascent version of the book-search application stopped in the debugger after a state change.

Figure 3. Debugging

In the debugger's console, you can see the state of the application, which for the moment is an object with two of the four properties that I listed previously: topic and displayMode. Each of those properties comes from a separate reducer that Redux combines into one, as shown in Listing 2.

Listing 2. Combined reducers (reducers.js)
import { combineReducers } from 'redux';

const defaults = {
  TOPIC:        'javascript',
  DISPLAY_MODE: 'THUMBNAIL',
}

const topicReducer = (state = defaults.TOPIC, action) => {
  switch(action.type) {
    case 'SET_TOPIC':
      return action.topic;

    default:
      return state;
  }
}

const displayModeReducer = (state = defaults.DISPLAY_MODE, action) => {
  switch(action.type) {
    case 'SET_DISPLAY_MODE':
      return action.displayMode;

    default:
      return state;
  }
}

// Combine reducers

export default combineReducers({
    topic:       topicReducer,
    displayMode: displayModeReducer
});

Listing 2 implements a topic reducer, which sets the topic part of the state; and a display mode reducer, which sets the display mode part of the state. Both the topic and the display mode are simple strings.

Recall that when you dispatch an action by calling the Redux dispatch() function, Redux calls the application's reducer function, passing the current state and the action. Actions, which specify a new state, typically have a type. In Listing 2, the action types are SET_TOPIC and SET_DISPLAY_MODE. Actions of the SET_TOPIC type have a topic property, and actions of the SET_DISPLAY_MODE type have a displayMode property, each specifying its new state. If you send an action to a reducer, and the reducer isn't interested — for instance, you send a SET_DISPLAY_MODE action to the topic reducer — the reducer returns the current state, unchanged.

After implementing the reducers, Listing 2 combines them with the Redux combineReducers() method. Notice that I pass an object to combineReducers().

The object that you pass to combineReducers() defines the state that Redux creates from the combined reducers. In this case, that state is an object with topic and displayMode properties. The value of the topic property is the state returned from the topic reducer, whereas the value of the displayMode property is the state returned from the display mode reducer.

Later in this series, I'll implement the book-search application's other two reducers and add them to the state object passed to combineReducers().

The application components

Now that you've seen how to combine reducers, I'll take you on a short detour to examine the application's components

The application component

The application, represented by the App component, is shown in Listing 3.

Listing 3. The App component (containers/app.js)
import React from 'react';
import ControlsContainer from './controls';

const titleStyle = {
  fontFamily: 'tahoma',
  fontSize: '24px',
  textAlign: 'center'
}

const Title = () => (
  <div style={titleStyle}>
    Book Search
  </div>
);

export const App = () => (
  <div>
    <Title />
    <hr/>
    <ControlsContainer />
  </div>
)

As is often case for React components that represent an entire application, the App component in Listing 3 simply contains other components — in this case, a title, a horizontal rule, and a controls container component.

The controls component

In Part 2, you saw that components in React/Redux applications are often made up of two separate components — a container that's connected to the Redux store and a stateless presentation component. And as you also know from Part 2, the benefits of stateless presentation components are substantial.

The controls component for the book-search application is likewise split into two components. Listing 4 shows the code for the connected container.

Listing 4. The controls container (containers/controls.js)
import { connect } from 'react-redux';
import Controls from '../components/controls';

const mapStateToProps = state => {
  return {
    topic: state.topic,
    displayMode: state.displayMode
  }
}

const mapDispatchToProps = null;

export default connect({
  mapStateToProps,
  mapDispatchToProps
)(Controls);

The controls container in Listing 4 maps application state to the Controls component's properties. As you know, the application state at this point consists of the topic and the application's display mode.

The controls container doesn't have any dispatch functionality to map to the properties of the Controls stateless component, so it passes a null value as the second argument to the Redux connect() function.

Listing 5 shows the code for the corresponding stateless component.

Listing 5. The stateless controls component (components/controls.js)
import React from 'react';
import DisplayModeContainer from '../containers/displayMode';
import TopicSelectorContainer from '../containers/topicSelector';

const Controls = ({
  topic,
  displayMode
}) => {
  const styles = {
    controls: {
      padding: '15px',
      marginBottom: '25px'
    }
  };

  return(
    <div style={styles.controls}>
      <TopicSelectorContainer topic={topic} />
      <DisplayModeContainer displayMode={displayMode} />
    </div>
  );
}

Controls.propTypes = {
  topic: React.PropTypes.string.isRequired
};

export default Controls;

The Controls stateless component contains the container components for the topic selector and the display mode. The interesting thing about the Controls component is its properties: topic and displayMode. Redux passes those properties to the stateless component by virtue of the call to connect() in Listing 4.

The topic selector component

Listing 6 shows the code for the container component for the topic selector.

Listing 6. The topic selector container (containers/topicselector.js)
import { connect } from 'react-redux';
import TopicSelector from '../components/topicSelector';
import { setTopic } from '../actions';

const mapStateToProps = state => {
  return {
    topic: state.topic
  }
}

const mapDispatchToProps = dispatch => {
  return {
    setTopic: topic => {
      dispatch(setTopic(topic))
    }
  }
}

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

Like all Redux container components, the TopicSelectorContainer maps state and dispatch functionality to the properties of its associated stateless component. In this case, the topic selector container maps the topic and a function that sets the topic.

Listing 7 shows the code for the topic selector stateless component.

Listing 7. The topic selector stateless component (components/topicselector.js)
import React from 'react';

export const TopicSelector = React.createClass({
  propTypes: {
    topic: React.PropTypes.string.isRequired
  },

  componentDidMount: function() {
    function putCursorAtEnd(input) {
      const value = input.value;
      input.value = '';
      input.value = value;
    }

    let input = this.refs.input;

    input.focus();
    putCursorAtEnd(input);
  },

  handleChange: function(event) {
    this.props.setTopic(event.target.value);
  },

  handleKeyPress: function(event) {
    if(event.key == 'Enter') {
      this.props.setTopic(event.target.value);
    }
  },

  render: function() {
    const styles = {
      topic: {
        marginRight: '10px',
        fontFamily: 'tahoma',
        fontSize: '18px'
      },

      input: {
        fontFamily: 'tahoma',
        fontSize: '16px',
        marginRight: '10px'
      }
    };

    const topic = this.props.topic;

    return(
      <span>
        <span style={styles.topic}>
          Topic
        </span>

        <input type='text'
               ref='input'
               style={styles.input}
               defaultValue={topic}
               value={topic}
               onChange={this.handleChange}
               onKeyPress={this.handleKeyPress}/>
      </span>
    );
  }
})

TopicSelector.propTypes = {
  topic: React.PropTypes.string.isRequired,
  setTopic: React.PropTypes.func.isRequired,
  fetchTopic: React.PropTypes.func.isRequired,
};

The topic selector stateless component contains a prompt and a text field. When React mounts the component, the component's componentDidMount() method gives the text field focus and places the cursor at the end of the text.

When the user presses Enter after typing in the text field, the component's handleKeyPress() method invokes the setTopic() method. Recall that the setTopic() method in the component's props exists because of the call to connect() in the stateless component's container.

The display mode component

Listing 8 shows the container component for the display mode.

Listing 8. The display mode container (containers/displayMode.js)
import { connect } from 'react-redux';
import DisplayMode from '../components/displayMode';
import { setDisplayMode } from '../actions';

const mapStateToProps = null;

const mapDispatchToProps = dispatch => {
  return {
    setListing: () => {
      dispatch(setDisplayMode('LISTING'))
    },

    setThumbnail: () => {
      dispatch(setDisplayMode('THUMBNAIL'))
    }
  }
}

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

The display mode container doesn't map state, but it does map two dispatch functions: setListing() and setThumbnail(), which dispatch actions that set the application's display mode to either listing or thumbnail.

Listing 9 shows the display mode stateless component.

Listing 9. The display mode stateless component (components/displayMode.js)
import React from 'react';

require('./book.css');

const DisplayMode = ({
  setListing,
  setThumbnail,
  displayMode
}) => {
  const switchToListing = function(event) {
    setListing();
  };

  const switchToThumbnail = function(event) {
    setThumbnail();
  };

  const styles = {
    radio: {
      marginLeft: '10px',
      cursor: 'pointer'
    },

    radiospan: {
      marginLeft: '20px',
      fontFamily: 'tahoma',
      fontSize: '16px'
    }
  };

  return(
    <span>
      <span style={styles.radiospan}>
        Thumbnail
      </span>

      <input type='radio'
             name='display'
             style={styles.radio}
             onChange={switchToThumbnail}
             checked={displayMode == 'THUMBNAIL'}
             value='thumbnail'/>

      <span style={styles.radiospan}>
        List
      </span>

      <input type='radio'
             name='display'
             style={styles.radio}
             onChange={switchToListing}
             checked={displayMode != 'THUMBNAIL'}
             value='list'/>
    </span>
  );
};

DisplayMode.propTypes = {
  setListing: React.PropTypes.func.isRequired,
  setThumbnail: React.PropTypes.func.isRequired,
  displayMode: React.PropTypes.string.isRequired
};

export default DisplayMode;

The stateless component in Listing 9 accesses three properties: setListing(), setThumbnail(), and displayMode. The display mode container passes along the setListing(), setThumbnail() properties. The displayMode property is set by the Controls stateless component, as you saw in Listing 5.

The current initial implementation of the book-search application illustrates combining reducers and implementing action creators. Next, you'll see how to implement asynchronous actions with Redux and React.

Asynchronous actions

Figure 4 shows the next version of the book-search application.

Figure 4. Searching asynchronously for books

As you know from Part 1, the book thumbnails shown in Figure 4 come from the Google Books API. By querying the googleapis.com URL directly in a browser — for example, typing http://www.googleapis.com/v11/volumes/?q=javascript as the web address — you can get the query results in JSON format, as shown in Figure 5.

Figure 5. Searching for books with the Google Books REST API

All of the app's actions are currently simple objects. For an asynchronous action, instead of an object I want a function that in turn dispatches actions as the asynchronous request progresses.

Listing 10 shows the updated entry point of the book-search application.

Listing 10. Fetching books on entry (index.js)
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import App from './containers/app';
import store from './store';
import { setTopic, setDisplayMode, fetchBooks } from './actions';

store.dispatch(setTopic('javascript'));
store.dispatch(setDisplayMode('THUMBNAIL'));
store.dispatch(fetchBooks());

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('example')
)

The only difference between Listing 10 and the previous version of the application's entry point is the dispatching of the fetch-books action. That action's creator is the fetchBooks() function, shown in Listing 11.

Listing 11. Asynchronous actions
/**
 * The fetchBooks action creator returns a function instead of an object.
 * Custom middleware is necessary for that to work.
 */
const fetchBooks = () => {
  return fetchCurrentTopic;
}

const fetchStart = () => {
  return {
    type: 'FETCH_STARTED'
  }
}

const fetchComplete = json => {
  return {
    type: 'FETCH_COMPLETE',
    json
  }
}

const fetchFailed = error => {
  return {
    type: 'FETCH_FAILED',
    error
  }
}

const setTopic = topic => {
  return {
    type: 'SET_TOPIC',
    topic
  }
}

const setDisplayMode = displayMode => {
  return {
    type: 'SET_DISPLAY_MODE',
    displayMode
  }
}

Listing 11 implements six action creators. Recall from Part 2 that action creators are functions that return actions.

The setTopic() and setDisplayMode() action creators are unchanged from their previous implementations. In addition to setTopic() and setDisplayMode(), three more of the six action creators implemented in Listing 11 return action objects. Those action creators return actions that specify states as the asynchronous fetching occurs:

  • fetchStart()
  • fetchComplete()
  • fetchFailed()

The sixth fetch action creator —fetchBooks()— returns a function instead of an object. That function, shown in Listing 12, uses the JavaScript fetch API to fetch books, but — more important— notice that it dispatches the other fetch actions at the appropriate points in time.

Listing 12. Fetching the current topic
const URL = 'https://www.googleapis.com/books/v1/volumes?q=';

const fetchCurrentTopic = (dispatch, state) => {
  dispatch(fetchStart())

  fetch(URL + state.topic)
    .then(res => {
      return res.json()
    })
    .then(json => {
      if(json.error) {
        dispatch(fetchFailed(json.error))
      }
      else {
        dispatch(fetchComplete(json))
      }
    })
    .catch(error => {
      dispatch(fetchFailed(json.error))
    })
}

The fetching actions, like all Redux actions, are processed (reduced) by a reducer. The application combines that reducer with the display mode and topic reducers.

Listing 13 shows a partial listing of reducers.js, showing how the fetch reducer handles the fetching actions.

Listing 13. The fetch reducer (reducers.js — partial listing)
import { combineReducers } from 'redux';

const defaults = {
  TOPIC:        'javascript',
  DISPLAY_MODE: 'THUMBNAIL',
  BOOKS:        []
}
...

const fetchReducer = (state = defaults.BOOKS, action) => {
  switch(action.type) {
    case 'FETCH_STARTED':
    case 'FETCH_FAILED':
      return [];

    case 'FETCH_COMPLETE':
      return action.json.items

    default:
      return state;
  }
}

// Combine reducers

export default combineReducers({
    topic:       topicReducer,
    displayMode: displayModeReducer,
    books:       fetchReducer
});

When a fetch either starts or fails, the fetch reducer returns an empty array. When the fetch is complete, the fetch reducer returns the items that were returned from the REST call.

To recap what I've just done:

I implemented a fetch action creator that returns a function, instead of an object, as is the case for most action creators. That function asynchronously fetches books using the Google Books API, and at various stages of the asynchronous fetch, it dispatches other actions by creating the actions with an action creator and dispatching it with the Redux dispatch() function.

The fetching actions are handled by the fetch reducer. When a fetch begins, the state returned by the fetch reducer is an empty array. When a fetch succeeds, the fetch reducer returns the books that were fetched. If a fetch fails, the fetch reducer resets the state to an empty array.

However, there's one problem. By default, Redux only supports actions that are objects, and not actions that are functions. If you try to run the code in this section, you'll an error:

Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.

Because Redux doesn't support actions that are functions, I need to teach it to do so — by implementing Redux middleware.

Redux middleware

As you know, to change application state with Redux, you dispatch actions. In response to an action dispatch, Redux invokes the application's reducer, sending it the current state and the action. The reducer subsequently returns the new state.

You know, too, that by default, actions must be plain JavaScript objects; they can't be functions. But when it comes to asynchronous actions, it would be most convenient if you could specify asynchronous actions as functions, as I did in the preceding section.

Fortunately, Redux provides a hook between dispatching actions and invoking the reducer. That hook is known as middleware, and applying it is simply a matter of calling Redux's applyMiddleware() function.

Recall from Part 1 that applyMiddleware() is a one of the five top-level Redux functions. You use applyMiddleware() to extend Redux through third-party middleware that intercepts dispatch calls. You can use this facility to implement many kinds of cross-cutting concerns — for example, logging, authentication, authorization, or collection of performance metrics — before you handle the main execution of the task.

I'll start by showing you how to use existing helpful middleware in the book-search app, and then I'll show you how to roll your own.

Using Redux middleware

You apply Redux middleware with the Redux applyMiddleware() function when you create the Redux store. Listing 14 shows how to use the redux-thunk middleware with the book-search application.

Listing 14. Using redux-thunk (store.js)
import { createStore, applyMiddleware } from 'redux'
import { reducers } from './reducers'
import thunk from 'redux-thunk'

export const store = createStore(reducers, applyMiddleware(thunk))

The redux-thunk middleware does exactly what I need in the book-search application — implement actions as functions in addition to plain JavaScript objects. You can install it with npm install redux-thunk.

Redux middleware is easy to use, as you can see from Listing 14. Now I'll show you how to implement your own middleware.

Implementing middleware

In Listing 15, I replace redux-thunk with custom middleware.

Listing 15. Using custom middleware (store.js)
import { createStore, applyMiddleware } from 'redux'
import { reducers } from './reducers'
import { thunk } from './middleware'

export const store = createStore(reducers, applyMiddleware(thunk))

Listing 16 shows the implementation of that custom middleware.

Listing 16. Thunk middleware (middleware.js)
export function thunk(store) {
  return function(dispatch) {
    return function(action) {
      if(typeof action === 'function') {
        action(dispatch, store.getState) // invoke the action
      }
      else {
        return dispatch(action) // dispatch normally
      }
    }
  }
}

Redux middleware — for example, thunk in Listing 16 — is a JavaScript function. More specifically, it's a function that returns a function that returns a function. You never invoke Redux middleware directly, but if you did, here's how you'd do it: thunk(store)(dispatch)(action).

Arrow functions make the implementation more pleasant to look at, although not necessarily easier to understand. Listing 17 shows the Listing 16 code rewritten with arrow functions.

Listing 17. Thunk middleware with arrow functions (middleware.js)
export const thunk = store => dispatch => action => {
  if(typeof action === 'function') {
    action(dispatch, store.getState) // invoke the action
  }
  else {
    return dispatch(action) // dispatch normally
  }
}

Whether you use arrow functions or not, to implement Redux middleware you must implement a function that returns a function that returns a function. Simply follow this recipe, where NAME represents the name of your middleware and ... is the code:

const NAME = store ⇒ dispatch ⇒ action ⇒ { ... }

Now that you know the mechanics of implementing Redux middleware, take a look at the implementation of the thunk middleware in Listing 16. Thanks to the magic of JavaScript closures, middleware functions have access to store (the Redux store), dispatch (the Redux dispatch() function), and action (the action that you're about to dispatch).

The thunk middleware checks to see if the action is a function; if it is, thunk invokes the action, passing the dispatch function and the store's getState() function. If the action isn't a function, the thunk middleware does what Redux normally does with actions — invokes the dispatch() function, passing the action.

Conclusion to Part 3

Redux applications change state by dispatching actions via the Redux dispatch() function. The dispatch() function, as its name implies, dispatches the action to the application's reducer, which creates a new application state. In this installment, you saw how to tap into that lifecycle and extend how Redux works by implementing Redux middleware that enables Redux to process actions that are functions — a handy facility for nearly all asynchronous actions.

In the next installment in this series, I show you how to implement time travel — at least from the perspective of your application's state — with Redux.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development
ArticleID=1035596
ArticleTitle=Manage state with Redux, Part 3: Implementing asynchronous actions with Redux
publish-date=08082016