In the preceding two installments in this series, you saw how to use and implement Redux middleware to execute asynchronous actions. You also saw how to implement time travel — the ability to move among your application’s previous states.

In this final installment in this series, you:

  • See how to round out the React version of the book-search application
  • Learn how to implement the book-search application with Redux and Angular
  • Get an introduction to the Redux DevTools and the Redux ecosystem

Rounding out the React version

From the previous installments, you understand the implementation of the controls and state viewer components, shown in Figure 1.

Figure 1. The book-search application
Screenshot of the React version of the book-search app

The controls are displayed at the top of the application, with the Topic text field on the left and the arrow buttons on the right. The state viewer components display the current application state at the bottom of the page. Now I’ll explain the Books component, which displays the books as either text or thumbnail links.

Recall from “Using Redux with React” that one way to connect React components to the Redux store is to implement them as two components: a container that contains a second component. The container connects to the Redux store and exports properties to the contained component when the state changes. The contained component creates a visual representation of those properties. The Books component is no exception. Listing 1 shows the code for the Books container.

Listing 1. The books container (containers/book.js)

import { connect } from 'react-redux';
import Books from '../components/books';

const mapStateToProps = state => ({
  books:         state.books,
  currentStatus: state.currentStatus,
  displayMode:   state.displayMode,
});

export default connect(
  mapStateToProps,
  null
)(Books);

The Books container maps state to properties of the contained Books component. Books don’t do anything other than make their presence known, so there are no actions to dispatch, which explains the null argument to the connect() function.

Listing 2 shows the code for the Books component that’s contained by the Books container.

Listing 2. The books component (components/books.js)

import React from 'react';
import Book from './book.js';

let ReactCSSTransitionGroup =
      require('react-addons-css-transition-group');

const Books = ({
  books,
  displayMode,
  currentStatus,
}) => {
  const styles = {
    container: {
      width: '100%',
    },

    spinner: {
      textAlign: 'center',

      width: '100%',
    },
  };

  const Spinner = () => (
    <div style={styles.spinner}>
      <img src="./images/spinner.gif"
        role="presentation" />
    </div>
  );

  const bookMarkup = () => {
    let components = null;
    let bookItems = (<span>No items!</span>);

    if (books.length > 0) {
      components = books.map(item => {
        if (item.volumeInfo.imageLinks) {
          // Need different keys for different display modes
          // to trigger <ReactCSSTransitionGroup> animations

          const key = displayMode === 'THUMBNAIL' ?
                                       item.id + 1 :
                                       item.id;
          bookItems = (
            <Book item={item}
              displayMode={displayMode}
              key={key} />);
        }
        return bookItems;
      });
    }
    return components;
  }

  return (
    <div>
      { currentStatus !== 'Fetching...' ?  null : <Spinner /> }

      <div style={styles.container}>
        <ReactCSSTransitionGroup transitionName="books"
          transitionLeaveTimeout={1}
          transitionEnterTimeout={1000}>
          {bookMarkup()}
        </ReactCSSTransitionGroup>
      </div>
    </div>
  );
};

Books.propTypes = {
  books:       React.PropTypes.array.isRequired,
  currentStatus: React.PropTypes.string.isRequired,
  displayMode: React.PropTypes.string.isRequired,
};

export default Books;

The Books component displays a spinner while the application is fetching the next round of books; otherwise, the component displays the books stored in the application’s state. The Books component uses the ReactCSSTransitionGroup component to fade the books in when they’re displayed.

From a Redux perspective, the most important things in Listing 2 are the three properties passed to the Books component: the list of books, the display mode, and the current fetch status. Those properties are available because of the preceding call to the connect() function in Listing 1. Recall that the connect() function doesn’t come with Redux but is provided by the react-redux bindings.

In Listing 2, the bookMarkup function creates markup with Book elements. The Book component’s code is in Listing 3.

Listing 3. The book component (components/book.js)

import React from 'react';

const Book = ({
  item,
  displayMode,
}) => {
  const styles = {
    links: {
      marginTop: '20px',
    },

    link: {
      padding: '25px',
    },

    image: {
      boxShadow: '3px 3px 3px #686868',
      marginBottom: '15px',
    },
  };

  const linkMarkup = (currentItem, link) => (
    <div style={styles.links}>
      <a href={link} style={styles.link}>
        {currentItem.volumeInfo.title}
      </a>
    </div>
  );

  const thumbnailMarkup = (currentItem, link) => (
    <a href={link} style={styles.link}>
      <img src={currentItem.volumeInfo.imageLinks.thumbnail}
        style={styles.image}
        role="presentation" />
    </a>
  );

  const link = item.volumeInfo.canonicalVolumeLink;

  return displayMode === 'THUMBNAIL' ?
           thumbnailMarkup(item, link) :
           linkMarkup     (item, link);
};

Book.propTypes = {
  item:        React.PropTypes.object.isRequired,
  displayMode: React.PropTypes.string.isRequired,
};

export default Book;

The Book component displays a book as either a link or a thumbnail. The item and displayMode properties come from the Books component, shown in Listing 2.

This component rounds out the implementation of the book-search application using React and Redux. React isn’t your only choice for a component framework to use with Redux. Next, you’ll see how to create the same application by using Redux with Angular. (If you’re strictly a React user, you can skip ahead to the Redux DevTools section.)

Using Redux with Angular

Figure 2 shows the Angular 2 (hereafter referred to simply as Angular) version of the book-search application.

Figure 2. Angular version of the book-search application
Screenshot of the Angular version of tyhe boo-search app

Angular, like React (and unlike Angular 1), is a component-based framework. In theory, then, I should be able to effectively swap out React components for Angular components and reuse the React version’s reducers, actions, and store. It turns out that the theory is pretty much borne out in practice.

Important note: The code for the book-search application will not work with any version of Angular before RC5.

The setup

The HTML for the Angular version of the book-search application is in Listing 4.

Listing 4. The application’s HTML (index.html)

<html>
  <head>
    <title>Book Search</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">
  </head>

  <body>
    <app>Loading...</app>
  </body>

  <!-- Polyfill(s) for older browsers -->
  <script src="node_modules/core-js/client/shim.min.js"></script>
  <script src="node_modules/zone.js/dist/zone.js"></script>
  <script src="node_modules/reflect-metadata/Reflect.js"></script>
  <script src="node_modules/systemjs/dist/system.src.js"></script>

  <script src="dist/bundle.js"></script>
</html>

The HTML in Listing 4 pulls in some polyfills and the application’s TypeScript. Listing 5 shows the application’s module.

Listing 5. The application’s module (app/app.module.ts)

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import App from './components/app.component';
import Book from './components/book.component';
import Books from './components/books.component';
import Controls from './components/controls.component';
import DisplayMode from './components/displayMode.component';
import History from './components/history.component';
import StateViewer from './components/stateviewer.component';
import TopicSelector from './components/topicselector.component';

@NgModule({
  bootstrap: [ App ],

  declarations: [
    App,
    Book,
    Books,
    Controls,
    DisplayMode,
    History,
    StateViewer,
    TopicSelector,
  ],

  imports: [
    BrowserModule,
    FormsModule,
  ],
})

export class AppModule { }

The Listing 5 code defines an Angular module. The @NgModule is a TypeScript annotation. (Annotations are heavily used in the TypeScript version of Angular.) The @NgModule annotation is a new feature in Angular RC5.

When you’re implementing an Angular application, you must declare all the components in your module. The Listing 5 code imports the browser module, which is imported by all Angular applications that run in a browser. That module is also the source of the @NgModule annotation.

I don’t use forms in the book-search application, but evidently I must import the forms module (FormsModule); if I don’t, I get the following error message:

Can’t bind to ngModel since it isn’t a known property of input.Can't bind to ngModel since it isn't a known property of input.

The error message gives no clue that you need to import the forms module; Stack Overflow led me to the solution.

The last piece of boilerplate is the application’s entry point (app/main.ts):


import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

The main.ts code bootstraps the Angular application.

That takes care of the boilerplate for the Angular version of the book-search application. Now I’ll show you the application’s Angular components.

The Angular components

You can pretty much swap out the React components in the original version for their Angular counterparts — and presto, you have an Angular book-search application. The reducers, actions, store, and state history are the same in the Angular version as in the React version.

As with React, with Angular you can implement components that you nest within one another. Listing 6 shows the App component, which contains all the other components in the application.

Listing 6. The App component (app/components/app.component.ts)

import { Component } from '@angular/core';

import Books from './books.component';
import Controls from './controls.component';
import StateViewer from './stateviewer.component';

import store from '../../store';
import { fetchBooks, setTopic } from '../../actions';

@Component({
  selector: 'app',
  template: 
    <div style='text-align: center; font-size: 1.5em'>
     {{app.title}}
    </div>

    <hr/>

    <controls></controls>
    <books></books>
    <state-viewer></state-viewer>
})

export default class {
  ngOnInit() {
    store.dispatch(setTopic('Border collies'));
    store.dispatch(fetchBooks());
  }

  app = {
    title: 'Book Search (Angular version)',
  };
}

The App component template contains three components: controls, books, and state-viewer. The component’s TypeScript dispatches actions to the Redux store when the App component has been initialized.

The controls component is shown in Listing 7.

Listing 7. The controls component (app/components/controls.component.ts)

import { Component } from '@angular/core';

@Component({
  selector: 'controls',
  template: 
    <topic-selector></topic-selector>
    <display-mode-container></display-mode-container>
    <history></history>

})

export default class Controls {
}

The controls component does nothing but contain other components, so the implementation of its associated class is empty (but still required).

The topic-selector component is shown in Listing 8.

Listing 8. The TopicSelector component (app/components/topicselector.component.ts)

import { Component } from '@angular/core';
import { fetchBooks, setTopic } from '../../actions';
import store from '../../store';

@Component({
  selector: 'topic-selector',
  template: 
    <label for='topic'>Topic</label>

    <input #topicInput
      [(ngModel)]='topic'
      (input)='setTopic(topicInput.value)'
      (keyup.enter)='fetchTopic(topicInput.value)'
      autofocus/>

})

export default class TopicSelector {
  private topic: string;
  private unsubscribe: any;

  constructor() {
    store.subscribe(() => {
      this.topic = store.getState().topic;
    });
  }

  setTopic(newTopic) {
    store.dispatch(setTopic(newTopic));
  }

  fetchTopic(newTopic) {
    store.dispatch(fetchBooks());
  }

  ngOnDestroy() {

    this.unsubscribe();
  }
}

The topic selector contains a label and an input for specifying a new topic. The input’s keyup.enter event is mapped to the component’s fetchTopic() method, and the input event is mapped to the component’s setTopic() method. As a result, the topic is updated for every keystroke, and a new fetch is initiated when the user hits the Enter key.

The [(ngModel)] construct is Angular’s special syntax for two-way binding. In this case, Angular transmits the value in the text field to the component’s topic property, and it also transmits changes to the property back to the text field.

From a Redux perspective, the most interesting thing about Listing 8 is the fact that the component subscribes to the Redux store. When the store changes, the component updates its topic property. As you’ll see, all of the Angular components in the book-search application subscribe to the Redux store in a similar fashion.

The display mode component is in Listing 9.

Listing 9. The DisplayMode component (app/components/displayMode.component.ts)

import { Component } from '@angular/core';
import store from '../../store';
import { setDisplayMode } from '../../actions';

@Component({
  selector: 'display-mode',
  template: 
    <span>
      <label for='thumbnailRadio'>Thumbnail</label>

      <input id="thumbnailRadio" style="cursor: pointer"
        type="radio"
        name="display_mode"
        value="Thumbnail"
        [checked]='displayMode === "THUMBNAIL"'
        (change)="setMode('THUMBNAIL')"/>

      <label for='listRadio'>List</label>

      <input id="listRadio" style="cursor: pointer"
        type="radio"
        name="display_mode"
        value="List"
        [checked]='displayMode === "LIST"'
        (change)="setMode('LIST')"/>
    </span>

})

export default class DisplayMode {
  private displayMode: string;
  private unsubscribe: any;

  constructor() {
    this.unsubscribe = store.subscribe(() => {
      this.displayMode = store.getState().displayMode;
    });
  }

  setMode(value) {
    store.dispatch(setDisplayMode(value));
  }

  ngOnDestroy() {
    this.unsubscribe();
  }
}

The display mode component is similar to the state viewer. Both components map input events to component methods. And both components subscribe to the Redux store to update a property when application state changes. The difference is that the display mode component has two inputs, which are radio buttons. Those radio buttons are linked to the display mode value stored in the application’s state.

The history component’s code is lengthy, so I’ve split it into two listings. The component’s template is shown in Listing 10.

Listing 10. The History component’s template (app/components/history.component.ts)

import { Component } from '@angular/core';

@Component({
  selector: 'history',
  template: 
  <input type='range' #range
    style='cursor: pointer'
    min={1}
    (input)='setState(range.value)'
    [max]='maximum'
    [value]='value'/>

  <a href='#' style='text-decoration: none'
    (click)='previousState()'
    [innerHTML] = 'leftArrow'>
  </a>

  <a href='#' style='text-decoration: none'
    (click)='nextState()'
    [innerHTML] = 'rightArrow'>
  </a>

})

The history component contains the slider and arrow buttons that control the current state of the application. Once again, the template markup maps events — specifically, click and input events — to component methods. Those methods are shown in Listing 11, which contains the history component’s TypeScript.

Listing 11. The History component’s TypeScript (app/components/history.component.ts)

import { gotoState, redo, undo } from '../../actions';
import stateHistory from '../../statehistory';
import store from '../../store';

export default class History {
  private leftArrow: string;
  private rightArrow: string;
  private stateHistory: any;
  private topic: string;
  private unsubscribe: any;
  private value: number;
  private maximum: number;

  constructor() {
    this.leftArrow = '←';
    this.rightArrow = '→';

    this.stateHistory = stateHistory;

    this.unsubscribe = store.subscribe(() => {
      this.topic = store.getState().topic;
      this.maximum = this.max();
      this.value = this.val();
    });
  }

  setState(stateIndex) {
    store.dispatch(gotoState(stateIndex));
  }

  previousState() {
    store.dispatch(undo());
  }

  nextState() {
    store.dispatch(redo());
  }

  val() {
    return this.stateHistory.past ? this.stateHistory.past.length : 0;
  }

  max() {
    return (this.stateHistory.past    ? this.stateHistory.past.length   : 0) +
           (this.stateHistory.present ? 1 : 0)             +
           (this.stateHistory.future  ? this.stateHistory.future.length : 0) - 1;
  }


  ngOnDestroy() {
    this.unsubscribe();
  }
}

The history component, like the other Angular components that I’ve discussed so far, subscribes to the Redux store and updates its topic, maximum, and value properties when the state changes.

The history component uses the stateHistory object to configure its slider. Like the reducers, actions, and store, the stateHistory object is unchanged from the React version of the application. See “Implementing time travel with Redux” for the implementation of the stateHistory object.

The Books component is shown in Listing 12.

Listing 12. The Books component (app/components/books.component.ts)

import { Component } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/core';
import store from '../../store';
import Book from './book.component';

@Component({
  selector: 'books',
  template: 
    <div *ngIf='status === "Fetching..."'
      style='width: 100%; padding: 20px; text-align: center'>
      <img src="../images/spinner.gif" role="presentation" />
    </div>

    <div *ngIf='displayMode === "LIST"' style='width: 100%; padding: 20px;'>
      <ul>
        <li *ngFor='let book of books'>
          <book [item]='book'
                [displayMode]='displayMode'></book>
        </li>
      </ul>
    </div>

    <div *ngIf='displayMode === "THUMBNAIL"' style='padding: 20px;'>
      <book *ngFor='let book of books'
        [item]='book'
        [displayMode]='displayMode'>
      </book>
    </div>

})

export default class Books {
  private books: Array<Book>;
  private displayMode: string;
  private status: string;
  private unsubscribe: any;

  constructor() {
    this.unsubscribe = store.subscribe(() => {
      const state = store.getState();

      this.books = state.books;
      this.displayMode = state.displayMode;
      this.status = state.currentStatus;
    });
  }

  ngOnDestroy() {
    this.unsubscribe();
  }
}

The Books component shows a spinner if the application’s current status is Fetching… The component also iterates over the list of books, creating book elements. The Books component synchronizes its properties representing states by subscribing to the Redux store.

The Book component is shown in Listing 13.

Listing 13. The Book component (app/components/book.component.ts)

import { Component, Input } from '@angular/core';
import store from '../../store';

@Component({
  selector: 'book',
  template: 
    <span *ngIf='displayMode === "THUMBNAIL"' style='padding: 10px'>
      <a href={{item.volumeInfo.canonicalVolumeLink}}>
        <img src={{item.volumeInfo.imageLinks.thumbnail}}/>
      </a>
    </span>

    <span *ngIf='displayMode === "LIST"'>
      <a href={{item.volumeInfo.canonicalVolumeLink}}>
        {{item.volumeInfo.title}}
      </a>
    </span>

})

export default class Book {
  @Input() item: any;
  @Input() displayMode: string;
}

The Book component’s markup renders a book as a text link or a thumbnail. The component’s TypeScript declares two input properties: item and displayMode. The values of those properties are specified by the Books component, shown in Listing 12.

The final component is the StateViewer, shown in Listing 14.

Listing 14. The StateViewer component (app/components/stateviewer.component.ts)

import { Component } from '@angular/core';
import store from '../../store';
import stateHistory from '../../statehistory';

@Component({
  selector: 'state-viewer',
  template: 
  <hr/>

  <span style='font-style: tahoma; font-size: 1.5em'>
    Application State
  </span>

  <div style='padding-top: 10px'>
    Topic: {{state.topic}}<br />
    Display mode: {{state.displayMode}}<br />
    Books displayed:   {{ state.books.length }}<br />
    Actions processed: {{ stateHistory.past.length +
                          stateHistory.future.length + 1 }}<br />
    Current action:    {{ stateHistory.past.length + 1 }}
  </div>

})

export default class StateViewer {
  private state: any;
  private stateHistory: any;
  private unsubscribe: any;

  constructor(
  ){
    this.state = store.getState();
    this.stateHistory = stateHistory;

    this.unsubscribe = store.subscribe(() => {
      this.state = store.getState();
    });
  }

  ngOnDestroy() {
    this.unsubscribe();
  }
}

The StateViewer component uses the application state and the state history to display the current state.

The Angular version of the book-search app is now complete.

Redux DevTools

Some of the book-search application’s functionality — a history slider and arrow buttons for moving among the application’s previous states, and a display of the current state — isn’t specific to searching for books. You could extract the code for these features and use it in other applications. And as you might suspect, someone — the implementer of Redux — has done something similar, by implementing Redux DevTools.

Figure 3 shows the Redux DevTools Chrome extension, which is built on top of the Redux DevTools.

Figure 3. The Redux DevTools Chrome extension
Screenshot of the Redux DevTools Chrome extension

Figure 3 shows the Log monitor pane, which by default displays the state history and provides a slider and arrow buttons. By now, you should have a good understanding of how those features are implemented.

Figure 4 shows the Chart pane, which provides a graphical representation of your application’s state. When you hover the cursor over individual data in the state, you can see the data’s representation in a pop-up window, which is also shown in Figure 4.

Figure 4. Exploring state with the Redux DevTools Chrome extension
Screenshot of viewing state with the Redux Devottols Chrome extension

Using the Redux DevTools Chrome extension is easy. After installing the extension, you must make one change to your application’s code, as outlined in the extension’s documentation. For the book-search application, which uses custom middleware, you simply modify the call to Redux’s createStore() function, as shown in Listing 15.

Listing 15. The book component (store.js)

import { createStore, applyMiddleware, compose } from 'redux';
import reducers from './reducers';
import { logger, thunk } from './middleware';

export default createStore(reducers, compose(applyMiddleware(logger, thunk),
      window.devToolsExtension ? window.devToolsExtension() : f => f));

Subsequently, when you open the developer tools in Chrome, you see a Redux tab. Click the tab to display the extension.

The Redux DevTools make Redux even more powerful by providing a potent set of debugging utilities. But that’s not all you get with Redux. Redux has also engendered a thriving ecosystem.

A brief tour of the Redux ecosystem

The Redux ecosystem includes the following repositories:

Some libraries in the Redux ecosystem, such as redux-form, are React-specific; others, such as redux-logic, only require Redux.

As you know, Redux maintains state with actions and reducers. Actions describe state changes and reducers produce an entirely new state, given the current state and an action. For synchronous actions, that simple model suffices, but as you saw in “Implementing asynchronous actions with Redux,” it doesn’t account for asynchronous actions.

Asynchronous actions are a special class of side effects, and the preceding list of Redux-related repositories — with the exception of redux-form, which uses Redux to store React form state — all deal with managing side effects. Those libraries facilitate features such as debouncing, which the book-search application doesn’t implement (but probably should, as pointed out by the author of redux-logic in a comment to “Implementing asynchronous actions with Redux“). With redux-saga, you’ll become familiar with Elm effects; and with redux-observable, you’ll learn how to integrate observables with Redux. Further discussion of the Redux ecosystem is beyond the scope of this series.

Conclusion

Although it’s not a glamorous topic, state management is one of the most crucial aspects of any application. Redux, with 22,000 stars and 3,500 forks on GitHub at the time of this writing, is one of the most popular JavaScript repositories.

Redux is so popular because it makes state management predictable, greatly reducing the number of bugs in your applications. Redux is also a simple JavaScript library that can be used with any UI framework. In this series you’ve seen how to use Redux to maintain both synchronous and asynchronous actions, and you’ve seen the same Redux-related code used with both React and Angular. Redux DevTools and the Redux ecosystem make Redux even more exciting.