Implementing Undo History

    This means that implementing Undo and Redo in an MVC application usually forces you to rewrite parts of your application to use a specific data mutation pattern like Command.

    With Redux, however, implementing undo history is a breeze. There are three reasons for this:

    • There are no multiple models—just a state subtree that you want to keep track of.
    • The state is already immutable, and mutations are already described as discrete actions, which is close to the undo stack mental model.
    • The reducer signature makes it natural to implement generic “reducer enhancers” or “higher order reducers”. They are functions that take your reducer and enhance it with some additional functionality while preserving its signature. Undo history is exactly such a case.
      Before proceeding, make sure you have worked through the and understand reducer composition well. This recipe will build on top of the example described in the .

    In the first part of this recipe, we will explain the underlying concepts that make Undo and Redo possible to implement in a generic way.

    In the second part of this recipe, we will show how to use Redux Undo package that provides this functionality out of the box.

    Undo history is also part of your app's state, and there is no reason why we should approach it differently. Regardless of the type of the state changing over time, when you implement Undo and Redo, you want to keep track of the history of this state at different points in time.

    For example, the state shape of a counter app might look like this:

    If we wanted to implement Undo and Redo in such an app, we'd need to store more state so we can answer the following questions:

    • Is there anything left to undo or redo?
    • What is the current state?
    • What are the past (and future) states in the undo stack?
      It is reasonable to suggest that our state shape should change to answer these questions:
    1. {
    2. counter: {
    3. past: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
    4. present: 10,
    5. future: []
    6. }
    7. }

    Now, if user presses “Undo”, we want it to change to move into the past:

    1. {
    2. counter: {
    3. past: [0, 1, 2, 3, 4, 5, 6, 7, 8],
    4. present: 9,
    5. future: [10]
    6. }
    7. }

    And further yet:

    1. {
    2. counter: {
    3. past: [0, 1, 2, 3, 4, 5, 6, 7],
    4. present: 8,
    5. future: [9, 10]
    6. }
    7. }

    When the user presses “Redo”, we want to move one step back into the future:

    1. {
    2. counter: {
    3. past: [0, 1, 2, 3, 4, 5, 6, 7, 8],
    4. present: 9,
    5. future: [10]
    6. }
    7. }

    Finally, if the user performs an action (e.g. decrement the counter) while we're in the middle of the undo stack, we're going to discard the existing future:

    1. {
    2. counter: {
    3. past: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
    4. present: 8,
    5. future: []
    6. }
    7. }

    The interesting part here is that it does not matter whether we want to keep an undo stack of numbers, strings, arrays, or objects. The structure will always be the same:

    1. {
    2. counter: {
    3. past: [0, 1, 2],
    4. present: 3,
    5. future: [4]
    6. }
    7. }
    1. {
    2. todos: {
    3. past: [
    4. [],
    5. [{ text: 'Use Redux' }],
    6. [{ text: 'Use Redux', complete: true }]
    7. ],
    8. present: [
    9. { text: 'Use Redux', complete: true },
    10. { text: 'Implement Undo' }
    11. ],
    12. future: [
    13. [
    14. { text: 'Use Redux', complete: true },
    15. { text: 'Implement Undo', complete: true }
    16. ]
    17. ]
    18. }
    19. }

    It is also up to us whether to keep a single top-level history:

    1. {
    2. past: [
    3. { counterA: 1, counterB: 1 },
    4. { counterA: 1, counterB: 0 },
    5. { counterA: 0, counterB: 0 }
    6. ],
    7. present: { counterA: 2, counterB: 1 },
    8. future: []
    9. }

    Or many granular histories so user can undo and redo actions in them independently:

    1. {
    2. counterA: {
    3. past: [1, 0],
    4. present: 2,
    5. },
    6. counterB: {
    7. past: [0],
    8. present: 1,
    9. future: []
    10. }
    11. }

    We will see later how the approach we take lets us choose how granular Undo and Redo need to be.

    Designing the Algorithm

    Regardless of the specific data type, the shape of the undo history state is the same:

    1. {
    2. past: Array<T>,
    3. present: T,
    4. future: Array<T>
    5. }

    Let's talk through the algorithm to manipulate the state shape described above. We can define two actions to operate on this state: UNDO and REDO. In our reducer, we will do the following steps to handle these actions:

    Handling Undo

    • Remove the last element from the past.
    • Set the present to the element we removed in the previous step.
    • Insert the old present state at the beginning of the future.

    Handling Redo

    • Remove the first element from the future.
    • Set the present to the element we removed in the previous step.
    • Insert the old present state at the end of the past.

    Handling Other Actions

    • Insert the present at the end of the past.
    • Set the present to the new state after handling the action.
    • Clear the future.

    First Attempt: Writing a Reducer

    1. const initialState = {
    2. past: [],
    3. present: null, // (?) How do we initialize the present?
    4. }
    5. function undoable(state = initialState, action) {
    6. const { past, present, future } = state
    7. switch (action.type) {
    8. case 'UNDO':
    9. const previous = past[past.length - 1]
    10. const newPast = past.slice(0, past.length - 1)
    11. return {
    12. past: newPast,
    13. present: previous,
    14. future: [present, ...future]
    15. }
    16. case 'REDO':
    17. const next = future[0]
    18. const newFuture = future.slice(1)
    19. return {
    20. past: [...past, present],
    21. present: next,
    22. future: newFuture
    23. }
    24. default:
    25. // (?) How do we handle other actions?
    26. return state
    27. }
    28. }

    This implementation isn't usable because it leaves out three important questions:

    • Where do we get the initial present state from? We don't seem to know it beforehand.
    • Where do we react to the external actions to save the present to the past?
    • How do we actually delegate the control over the present state to a custom reducer?
      It seems that reducer isn't the right abstraction, but we're very close.

    You might be familiar with . If you use React, you might be familiar with higher order components. Here is a variation on the same pattern, applied to reducers.

    A reducer enhancer (or a higher order reducer) is a function that takes a reducer, and returns a new reducer that is able to handle new actions, or to hold more state, delegating control to the inner reducer for the actions it doesn't understand. This isn't a new pattern—technically, is also a reducer enhancer because it takes reducers and returns a new reducer.

    A reducer enhancer that doesn't do anything looks like this:

    1. function doNothingWith(reducer) {
    2. return function(state, action) {
    3. // Just call the passed reducer
    4. return reducer(state, action)
    5. }
    6. }

    A reducer enhancer that combines other reducers might look like this:

    1. function combineReducers(reducers) {
    2. return function(state = {}, action) {
    3. return Object.keys(reducers).reduce((nextState, key) => {
    4. // Call every reducer with the part of the state it manages
    5. nextState[key] = reducers[key](state[key], action)
    6. return nextState
    7. }, {})
    8. }
    9. }

    Second Attempt: Writing a Reducer Enhancer

    Now that we have a better understanding of reducer enhancers, we can see that this is exactly what undoable should have been:

    1. function undoable(reducer) {
    2. // Call the reducer with empty action to populate the initial state
    3. const initialState = {
    4. past: [],
    5. present: reducer(undefined, {}),
    6. future: []
    7. }
    8. // Return a reducer that handles undo and redo
    9. return function(state = initialState, action) {
    10. const { past, present, future } = state
    11. switch (action.type) {
    12. case 'UNDO':
    13. const previous = past[past.length - 1]
    14. const newPast = past.slice(0, past.length - 1)
    15. return {
    16. past: newPast,
    17. present: previous,
    18. future: [present, ...future]
    19. }
    20. case 'REDO':
    21. const next = future[0]
    22. const newFuture = future.slice(1)
    23. return {
    24. past: [...past, present],
    25. present: next,
    26. future: newFuture
    27. }
    28. default:
    29. const newPresent = reducer(present, action)
    30. if (present === newPresent) {
    31. return state
    32. }
    33. return {
    34. past: [...past, present],
    35. present: newPresent,
    36. future: []
    37. }
    38. }
    39. }
    40. }

    We can now wrap any reducer into undoable reducer enhancer to teach it to react to UNDO and REDO actions.

    There is an important gotcha: you need to remember to append .present to the current state when you retrieve it. You may also check .past.length and .future.length to determine whether to enable or to disable the Undo and Redo buttons, respectively.

    Using Redux Undo

    This was all very informative, but can't we just drop a library and use it instead of implementing undoable ourselves? Sure, we can! Meet Redux Undo, a library that provides simple Undo and Redo functionality for any part of your Redux tree.

    In this part of the recipe, you will learn how to make the undoable. You can find the full source of this recipe in the todos-with-undo example that comes with Redux.

    Installation

    First of all, you need to run

    1. npm install --save redux-undo

    This installs the package that provides the undoable reducer enhancer.

    You will need to wrap the reducer you wish to enhance with undoable function. For example, if you exported a todos reducer from a dedicated file, you will want to change it to export the result of calling undoable() with the reducer you wrote:

    reducers/todos.js

    1. /* ... */
    2. const todos = (state = [], action) => {
    3. /* ... */
    4. }
    5. const undoableTodos = undoable(todos, {
    6. filter: distinctState()
    7. })
    8. export default undoableTodos

    The distinctState() filter serves to ignore the actions that didn't result in a state change. There are to configure your undoable reducer, like setting the action type for Undo and Redo actions.

    Note that your combineReducers() call will stay exactly as it was, but the todos reducer will now refer to the reducer enhanced with Redux Undo:

    reducers/index.js

    1. import { combineReducers } from 'redux'
    2. import todos from './todos'
    3. import visibilityFilter from './visibilityFilter'
    4. const todoApp = combineReducers({
    5. todos,
    6. visibilityFilter
    7. })
    8. export default todoApp

    You may wrap one or more reducers in undoable at any level of the reducer composition hierarchy. We choose to wrap todos instead of the top-level combined reducer so that changes to visibilityFilter are not reflected in the undo history.

    Updating the Selectors

    Now the todos part of the state looks like this:

    1. {
    2. visibilityFilter: 'SHOW_ALL',
    3. todos: {
    4. past: [
    5. [],
    6. [{ text: 'Use Redux' }],
    7. [{ text: 'Use Redux', complete: true }]
    8. ],
    9. present: [
    10. { text: 'Use Redux', complete: true },
    11. { text: 'Implement Undo' }
    12. ],
    13. future: [
    14. [
    15. { text: 'Use Redux', complete: true },
    16. { text: 'Implement Undo', complete: true }
    17. ]
    18. ]
    19. }
    20. }

    This means you need to access your state with state.todos.present instead ofjust state.todos:

    containers/VisibleTodoList.js

    1. const mapStateToProps = state => {
    2. return {
    3. todos: getVisibleTodos(state.todos.present, state.visibilityFilter)
    4. }
    5. }

    Adding the Buttons

    Now all you need to do is add the buttons for the Undo and Redo actions.

    First, create a new container component called UndoRedo for these buttons. We won't bother to split the presentational part into a separate file because it is very small:

    containers/UndoRedo.js

    1. import React from 'react'
    2. /* ... */
    3. let UndoRedo = ({ canUndo, canRedo, onUndo, onRedo }) => (
    4. <p>
    5. <button onClick={onUndo} disabled={!canUndo}>
    6. Undo
    7. </button>
    8. <button onClick={onRedo} disabled={!canRedo}>
    9. Redo
    10. </button>
    11. </p>
    12. )

    You will use connect() from to generate a container component. To determine whether to enable Undo and Redo buttons, you can check state.todos.past.length and state.todos.future.length. You won't need to write action creators for performing undo and redo because Redux Undo already provides them:

    containers/UndoRedo.js

    1. /* ... */
    2. import { ActionCreators as UndoActionCreators } from 'redux-undo'
    3. import { connect } from 'react-redux'
    4. /* ... */
    5. const mapStateToProps = state => {
    6. return {
    7. canUndo: state.todos.past.length > 0,
    8. canRedo: state.todos.future.length > 0
    9. }
    10. }
    11. const mapDispatchToProps = dispatch => {
    12. return {
    13. onUndo: () => dispatch(UndoActionCreators.undo()),
    14. onRedo: () => dispatch(UndoActionCreators.redo())
    15. }
    16. }
    17. UndoRedo = connect(
    18. mapStateToProps,
    19. mapDispatchToProps
    20. )(UndoRedo)
    21. export default UndoRedo

    components/App.js

    This is it! Run npm install and in the example folder and try it out!