redux-undo is a reducer enhancer (higher-order reducer). It provides the undoable function, which takes an existing reducer and a configuration object and enhances your existing reducer with undo functionality.
Note: If you were accessing state.counter before, you have to access state.counter.present after wrapping your reducer with undoable.
combineReducers({ counter:undoable(counter, { limit:10// set a limit for the size of the history })})
Apply redux-undo magic to specific slice of your state.
When you expose an undo redo history action to your app users, you will not want those action to apply on your whole redux state. Lets see this with naive document editor state.
will provide only the document mountpoint of your state with an history.
an even more advanced usage would be to have many different mountpoint of your redux state, managed under redux-undo.
constrootReducer=combineReducers({ ui: uiReducer, document:undoable(documentReducer, { undoType:'DOCUMENT_UNDO', redoType:'DOCUMENT_REDO',// here you will want to configure specific redux-undo action type }), anotherDocument:undoable(documentReducer, { undoType:'ANOTHERDOCUMENT_UNDO', redoType:'ANOTHERDOCUMENT_REDO',// here you will want to configure specific redux-undo action type }),})
Don't forget to configure specific redux-undo action type for each of your mount point if you don't want to see your different history to undo/redo in sync.
History API
Wrapping your reducer with undoable makes the state look like this:
Now you can get your current state like this: state.present
And you can access all past states (e.g. to show a history) like this: state.past
Note: Your reducer still receives the current state, a.k.a. state.present. Therefore, you would not have to update an existing reducer to add undo functionality.
Undo/Redo Actions
Firstly, import the undo/redo action creators:
import { ActionCreators } from'redux-undo';
Then, you can use store.dispatch() and the undo/redo action creators to perform undo/redo operations on your state:
store.dispatch(ActionCreators.undo()) // undo the last actionstore.dispatch(ActionCreators.redo()) // redo the last actionstore.dispatch(ActionCreators.jump(-2)) // undo 2 stepsstore.dispatch(ActionCreators.jump(5)) // redo 5 stepsstore.dispatch(ActionCreators.jumpToPast(index)) // jump to requested index in the past[] arraystore.dispatch(ActionCreators.jumpToFuture(index)) // jump to requested index in the future[] arraystore.dispatch(ActionCreators.clearHistory()) // Remove all items from past[] and future[] arrays
Configuration
A configuration object can be passed to undoable() like this (values shown are default values):
undoable(reducer, { limit:false,// set to a number to turn on a limit for the historyfilter: () =>true,// see `Filtering Actions`groupBy: () =>null,// see `Grouping Actions` undoType:ActionTypes.UNDO,// define a custom action type for this undo action redoType:ActionTypes.REDO,// define a custom action type for this redo action jumpType:ActionTypes.JUMP,// define custom action type for this jump action jumpToPastType:ActionTypes.JUMP_TO_PAST,// define custom action type for this jumpToPast action jumpToFutureType:ActionTypes.JUMP_TO_FUTURE,// define custom action type for this jumpToFuture action clearHistoryType:ActionTypes.CLEAR_HISTORY,// define custom action type for this clearHistory action// you can also pass an array of strings to define several action types that would clear the history// beware: those actions will not be passed down to the wrapped reducers initTypes: ['@@redux-undo/INIT'],// history will be (re)set upon init action type// beware: those actions will not be passed down to the wrapped reducers debug:false,// set to `true` to turn on debugging ignoreInitialState:false,// prevent user from undoing to the beginning, ex: client-side hydration neverSkipReducer:false,// prevent undoable from skipping the reducer on undo/redo and clearHistoryType actions syncFilter: false // set to `true` to synchronize the `_latestUnfiltered` state with `present` when an excluded action is dispatched
})
Note: If you want to use just the initTypes functionality, but not import the whole redux-undo library, use redux-recycle!
Initial State and History
You can use your redux store to set an initial history for your undoable reducers:
Or just set the current state like you're used to with Redux. Redux-undo will create the history for you:
import { createStore } from'redux';conststore=createStore(undoable(counter), {foo:'bar'});// will make the state look like this:{ past: [], present: {foo:'bar'}, future: []}
Grouping Actions
If you want to group your actions together into single undo/redo steps, you can add a groupBy function to undoable. redux-undo provides groupByActionTypes as a basic groupBy function:
import undoable, { groupByActionTypes } from'redux-undo';undoable(reducer, { groupBy:groupByActionTypes(SOME_ACTION) })// or with arraysundoable(reducer, { groupBy:groupByActionTypes([SOME_ACTION]) })
In these cases, consecutive SOME_ACTION actions will be considered a single step in the undo/redo history.
Custom groupBy Function
If you want to implement custom grouping behaviour, pass in your own function with the signature (action, currentState, previousHistory). If the return value is not null, then the new state will be grouped by that return value. If the next state is grouped into the same group as the previous state, then the two states will be grouped together in one step.
If the return value is null, then redux-undo will not group the next state with the previous state.
The groupByActionTypes function essentially returns the following:
If a grouped action type (SOME_ACTION), the action type of the action (SOME_ACTION).
If not a grouped action type (any other action type), null.
When groupBy groups a state change, the associated group will be saved alongside past, present, and future so that it may be referenced by the next state change.
After an undo/redo/jump occurs, the current group gets reset to null so that the undo/redo history is remembered.
Filtering Actions
If you don't want to include every action in the undo/redo history, you can add a filter function to undoable. This is useful for, for example, excluding actions that were not triggered by the user.
redux-undo provides you with the includeAction and excludeAction helpers for basic filtering. They should be imported like this:
undoable(reducer, { filter:includeAction(SOME_ACTION) })undoable(reducer, { filter:excludeAction(SOME_ACTION) })// they even support Arrays:undoable(reducer, { filter:includeAction([SOME_ACTION,SOME_OTHER_ACTION]) })undoable(reducer, { filter:excludeAction([SOME_ACTION,SOME_OTHER_ACTION]) })
Note: Since beta4, only actions resulting in a new state are recorded. This means the (now deprecated) distinctState() filter is auto-applied.
Custom Filters
If you want to create your own filter, pass in a function with the signature (action, currentState, previousHistory). For example:
undoable(reducer, {filter:functionfilterActions(action, currentState, previousHistory) {returnaction.type ===SOME_ACTION; // only add to history if action is SOME_ACTION }})// The entire `history` state is available to your filter, so you can make// decisions based on past or future states:undoable(reducer, {filter:functionfilterState(action, currentState, previousHistory) {let { past, present, future } = previousHistory;returnfuture.length===0; // only add to history if future is empty }})
When implementing a filter function, it only prevents the old state from being stored in the history. filter does not prevent the present state from being updated.
If you want to ignore an action completely, as in, not even update the present state, you can make use of redux-ignore.
It can be used like this:
import { ignoreActions } from'redux-ignore'ignoreActions(undoable(reducer), [IGNORED_ACTION,ANOTHER_IGNORED_ACTION])// or define your own function:ignoreActions(undoable(reducer), (action) =>action.type ===SOME_ACTION// only add to history if action is SOME_ACTION)