At the end of Part 3 in this series, I was in the midst of implementing the book-search application shown in Figure 1. You saw how to implement separate reducers for logical parts of the application’s state — the topic, the display mode, and the current fetch status — and how to combine those reducers into the one-per-application requisite Redux reducer.

Figure 1. Searching asynchronously for books
alt

You also saw how to implement the book-search app’s asynchronous fetch action as a function, instead of a mere (nonfunction) object. However, out of the box, Redux isn’t equipped to deal with actions that are functions, so I showed you how to use and implement Redux middleware for that purpose. In this fourth installment, I show you how a time-travel feature can be added to the application, thanks to Redux.

Redux time travel

Recall that Redux uses objects to represent states, and pure functions to compute the next application state. These characteristics make Redux a predictable state container, meaning that given a specific application state and a specific action, the next state of the application is always exactly the same. That predictability makes it easy to implement time travel — the ability to move back and forth among the application’s previous states and view the results in real time. Figure 2, which shows the final version of the book-search application, demonstrates this feature.

Figure 2. State history and undo/redo
alt

In the app’s final version, the controls component at the top of the page contains a history slider and arrows for moving forward and backward in time. Additionally, a state viewer component that shows the current application state is at the bottom of the page.

Figure 3 shows how you can move through the application’s previous states by manipulating the application’s arrows and slider.

Figure 3. Time travel
alt

Before searching for Dr. Seuss books, I searched for border collie, as you can see from the top picture in Figure 3. As you can tell from that picture and the middle picture in Figure 3, I backspaced over the border collie text, typed seuss, and hit Enter, causing the search to begin. The middle picture shows the application during that search. The bottom picture shows the latest state of the application, with the history slider’s thumb all the way to the right.

It’s not evident from the screenshots in Figure 3, but as you drag the slider’s thumb, you can see the text border collie become border colli, then border coll, then border col, and so forth until the text field is empty. Then, as you continue to drag the slider’s thumb, you see the text s followed by su, followed by sue, until it gets to suess. In effect, you can travel back and forth through time, at least from your application’s perspective.

Now that you’ve seen it in action, I’ll show you how to implement time travel with Redux.

The state history

Implementing time travel is relatively simple. Here’s what I’m going to do.

First, I’ll implement a state history object that maintains an array of past states, an array of future states, and a single state representing the present. Then I’ll modify the application to store all of its states in the state history object and wire the history slider and arrow buttons to state history methods.

Listing 1 shows the state history object.

Listing 1. The state history object (statehistory.js)

export default {
  past: [],

  present: undefined,
  future: [],

  thereIsAPresent:     function() { return this.present != undefined; },
  thereIsAPast:        function() { return this.past.length > 0; },
  thereIsAFuture:      function() { return this.future.length > 0; },
  setPresent:          function(state) { this.present = state; },
  movePresentToPast:   function() { this.past.push(this.present); },
  movePresentToFuture: function() { this.future.push(this.present); },
  movePastToPresent:   function() { this.setPresent(this.past.pop()); },
  moveFutureToPresent: function() { this.setPresent(this.future.pop()); },

  push: function(currentState) {
    if(this.thereIsAPresent()) {
      this.movePresentToPast();
    }
    this.setPresent(currentState);
  },

  undo: function() {
    if(this.thereIsAPresent()) {
      this.movePresentToFuture(); // Moving back in time
      this.movePastToPresent();   // Moving back in time
    }
  },

  redo: function() {
    if(!this.thereIsAFuture()) { // No future!
      return;
    }

    if(this.thereIsAPresent()) {
      this.movePresentToPast(); // Moving forward in time
    }

    this.moveFutureToPresent(); // Moving forward in time
  },

  gotoState: function(i) {
    const index = Number(i);
    const allstates = [...this.past, this.present, ...this.future];

    this.present = allstatesindex    this.past = allstates.slice(0, index)
    this.future = allstates.slice(index+1, allstates.length)
  }
}

The state history object is a stack of states with the following four methods:

  • push(state)
  • undo()
  • redo()
  • gotoState(stateIndex)

Whenever the book-search application dispatches an action, it invokes the state history object’s push() method to push the current state onto the state history object’s internal stack.

Undo/redo

For the undo/redo arrows in the controls component, the application needs the state history object’s undo() and redo() methods. For the history slider, I need to be able to jump to any state in the state history, so the state history object provides a gotoState() method.

The StateHistory.push() method sets the current state to the present. If there’s a present state to begin with, the method moves it into the past.

The undo() method does nothing if there’s no present state. Otherwise, it moves the present state to the future and subsequently moves the past to the present.

The redo() method does nothing if there’s no future; otherwise, it moves the present to the past and subsequently moves the future to the present.

Finally, the gotoState() method goes directly to the state corresponding to the index passed to the function. The gotoState() method uses the JavaScript spread operator to store all states into one array and then splits the array into past, present, and future, depending on the index.

Now that I’ve discussed the implementation of the state history object, I’ll return to the implementation of the book-search application.

The book-search app with history and undo/redo

Listing 2 shows the final implementation of the book-search application’s App component.

Listing 2. The final book-search application (containers/app.js)

import React from 'react';
import ControlsContainer from './controls';
import BooksContainer from './books';
import StateViewerContainer from './stateviewer';

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

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

export default () => (
  <div>
    <Title />
    <hr/>
    <ControlsContainer    />
    <BooksContainer       />
    <StateViewerContainer />
  </div>
)

The App component contains three components: the ControlsContainer, the BooksContainer, and the StateViewerContainer. The ControlsContainer contains the history slider and arrow buttons, and the StateViewerContainer contains a stateless component that displays the current state. I’ll begin with StateViewerContainer.

The state viewer component

Listing 3 shows the state viewer container:

Listing 3. The state viewer container (containers/stateviewer.js)

import { connect } from 'react-redux';
import StateViewer from '../components/stateviewer';
import stateHistory from '../statehistory';

const mapStateToProps = state => {
  return {
    books:         state.books,
    topic:         state.topic,
    currentStatus: state.currentStatus,
    displayMode:   state.displayMode,
    history:       stateHistory
  }
}

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

The stateviewer container maps five properties to its enclosed stateless counterpart. The state viewer stateless component uses those properties in Listing 4.

Listing 4. The stateviewer stateless component (components/stateviewer.js)

import React from 'react';

const StateViewer = ({
  topic,
  books,
  currentStatus,
  displayMode,
  history
}) => {
  const styles = {
    container: {
      margin: '20px',
      width: '400px',
      fontFamily: 'tahoma'
    },

    title: {
      fontSize: '24px',
      marginTop: '25px'
    },

    state: {
      marginTop: '10px'
    },

    hr: {
      marginTop: '50px'
    }
  };

  return(
    <div style={styles.container}>
      <hr style={styles.hr}/>

      <div style={styles.title}>
        Application State
      </div>

      <div style={styles.state}>
        Topic: {topic}<br/>

        Display mode:      { displayMode }<br/>
        Current status:    { currentStatus }<br/>
        Books displayed:   { books.length }<br/>
        Actions processed: { history.past.length + history.future.length + 1 }<br/>
        Current action:    { history.past.length + 1 }
      </div>
    </div>
  );
}

StateViewer.propTypes = {
  books: React.PropTypes.array.isRequired,
  currentStatus: React.PropTypes.string.isRequired,
  displayMode: React.PropTypes.string.isRequired,
  history: React.PropTypes.object.isRequired,
  topic: React.PropTypes.string.isRequired,
};

export default StateViewer;

The component in Listing 4 is simple; it merely displays its properties, which come from the enclosing StateViewerContainer, which maps them from the current state.

That’s it for the state viewer that displays application state at the bottom of the page. Next I return to the more interesting controls component.

The state controls

In the previous article in this series, I discussed the controls component, which — like many components in a Redux application — is split into a container component and an enclosed stateless component. The container component maps topic and display mode to the stateless component. In addition, that stateless component (shown in Listing 5) includes the HistoryContainer component, where all the time travel action happens.

Listing 5. The controls stateless component (components/controls.js)

import React from 'react';
import DisplayModeContainer from '../containers/displayMode';
import TopicSelectorContainer from '../containers/topicSelector';
import HistoryContainer from '../containers/history';

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

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

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

export default Controls;

The history container and stateless component

The HistoryContainer component is shown in Listing 6.

Listing 6. The history container (containers/history.js)

import { connect } from 'react-redux';
import { undo, redo, gotoState } from '../actions';
import { History } from '../components/history';
import stateHistory from '../statehistory';

const mapStateToProps = state => {
  return {
    past: stateHistory.past,
    present: stateHistory.present,
    future: stateHistory.future
  }
}

const mapDispatchToProps = dispatch => {
 return {
   undo: () => {
     dispatch(undo());
   },

   redo: () => {
     dispatch(redo());
   },

  gotoState: stateIndex => {
     dispatch(gotoState(stateIndex));
   }
 }
}

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

The history container maps past, present, and future as properties to its contained History component. It also maps three functions —undo(), redo(), and gotoState()— as properties for the History component.

Notice that the functions that the history container maps to its stateless component all dispatch Redux actions. Implementing time travel involves changing state, and the functions in Listing 6 do so by dispatching Redux actions. The actions are created by action creators shown in Listing 7.

Listing 7. History action creators (actions.js)

...

export const redo = () => {
  return {
    type: 'REDO'

  }
}

export const undo = () => {
  return {
    type: 'UNDO'
  }
}

export const gotoState = stateIndex => {
  return {
    type: 'GOTO',
    stateIndex
  }
}

As is often the case for action creators, the ones in Listing 7 are simple — they merely return action objects. Only the gotoState action provides information other than the type of the state change. That information is the index into the array of states maintained by the history object.

Listing 8 shows the history stateless component.

Listing 8. The history stateless component (components/history.js)

import React from 'react';

export const History = ({
  past, present, future,
  undo, redo, gotoState,
}) => {
  const styles = {
    container: {
      marginLeft: '20px',
      cursor: 'pointer',
    },

    link: { textDecoration: 'none' },
    input: { cursor: 'pointer' }
  }

  const RightArrow = () => {
    return(
      <a href='#' style={styles.link}
                  onClick={() => redo()}> →
      </a>
    );
  }

  const LeftArrow = () => {
    return(
      <a href='#' style={styles.link}
                  onClick={() => undo()}> ←
      </a>
    );
  }

  const maxRange = () => {
    return (past   ? past.length   : 0) +
           (present? 1 : 0)             +
           (future ? future.length : 0) - 1;
  }

  return(
    <span style={styles.container}>
      History

      <input type='range'
             style={styles.input}
             min={0}
             max={maxRange()}
             value={past ? past.length : 0}
             onChange={event => gotoState(event.target.value)}/>

      { (past   && past.length   > 0) ? <LeftArrow />  : null }
      { (future && future.length > 0) ? <RightArrow /> : null }
    </span>
  )
}

History.propTypes = {
  past: React.PropTypes.array.isRequired,
  present: React.PropTypes.object.isRequired,
  future: React.PropTypes.array.isRequired,

  undo: React.PropTypes.func.isRequired,
  redo: React.PropTypes.func.isRequired,
  gotoState: React.PropTypes.func.isRequired,
};

The history stateless component displays the history slider and the arrows. When the user clicks an arrow, this component invokes the corresponding undo() or redo() functions that it receives as properties from its surrounding container component.

When the user manipulates the slider, the history stateless component invokes the gotoState() function, which it also receives as a property from its surrounding container.

So far, in the quest to implement history and undo/redo functionality, I’ve implemented a state history object that keeps track of past, present, and future. And I’ve looked at the implementations of the React components that contain the controls to manipulate the current state and the HTML elements that display that state.

One key piece of functionality remains: handling the undo, redo, and goto actions defined in Listing 7. Next, I show you how.

Higher-order reducers

In Part 3, you saw how to combine reducers with the Redux combineReducers() function. The final version of the book-search application combines four reducers, as shown in Listing 9.

Listing 9. The combined reducers (reducers.js)

Listing 10 shows the revised reducers for the book-search application after time travel is incorporated.

Listing 10. The higher-order undo reducer (reducers.js)

import { combineReducers } from 'redux';
import { store } from './store';
import stateHistory from './statehistory';

...

const undo = reducer => (state = stateHistory.present, action) => {
  switch(action.type) {
    case 'UNDO':
      stateHistory.undo();
      break;

    case 'REDO':
      stateHistory.redo();
      break;

    case 'GOTO':
      stateHistory.gotoState(action.stateIndex);
      break;

    default:
      const newState = reducer(state, action);
      stateHistory.push(newState);
  }

  return stateHistory.present;
}

// Combine reducers

export default undo(combineReducers({
    books:         fetchReducer,
    topic:         topicReducer,
    currentStatus: statusReducer,
    displayMode:   bookDisplayReducer
}));

In Listing 10, the undo() function is a reducer that takes another reducer as a parameter. The undo function uses that reducer to compute the next state when the current action is not history-related, meaning the action’s type is not UNDO, REDO, or GOTO. In that case, the undo function pushes that state onto the history stack after it’s calculated by the enclosed reducer.

When the action is history-related, the undo function delegates to the appropriate state history method. Whether the current action is history-related or not, the undo() function returns the present state as currently defined by the stateHistory object.

Reducers, like the undo function, that use another reducer to determine the next state are known as higher-order reducers, which you might recognize as a functional equivalent of the Decorator pattern.

Conclusion to Part 4

In this article, you saw how to implement time travel. Although it’s probably not as exciting as real time travel would be, the ability to easily implement code that enables you to travel back and forth among your application’s previous states is impressive. This feature might be more of a boon to developers than to their users, because it opens the door to a time-traveling debugger.

In the next installment, I’ll round out the implementation of the book-search application and introduce Redux developer tools. You’ll also learn about using Redux with other frameworks, such as Angular.