Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork187
♻️ higher order reducer to add undo/redo functionality to redux state containers
License
omnidan/redux-undo
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
simple undo/redo functionality for redux state containers
Protip: Check out thetodos-with-undo example or theredux-undo-boilerplate to quickly get started withredux-undo.
Switching from 0.x to 1.0: Make sure to update your programs to thelatest History API.
Help wanted: We are looking for volunteers to maintain this project, if you are interested, feel free to contact me atme@omnidan.net
This README is about the new 1.0 branch of redux-undo, if you are usingor plan on using 0.6, check outthe0.6 branch
If you use Redux Undo in CommonJS environment,don’t forget to add.default to your import.
- var ReduxUndo = require('redux-undo')+ var ReduxUndo = require('redux-undo').default
If your environment support es modules just go by:
importReduxUndofrom'redux-undo';
We are also supporting UMD build:
varReduxUndo=window.ReduxUndo.default;
once again.default is required.
npm install --save redux-undoimportundoablefrom'redux-undo';undoable(reducer)undoable(reducer,config)
redux-undo is a reducer enhancer (higher-order reducer). It provides theundoable function, whichtakes an existing reducer and a configuration object and enhances your existingreducer with undo functionality.
Note: If you were accessingstate.counter before, you have to accessstate.present.counter after wrapping your reducer withundoable.
To install, firstly importredux-undo:
// Redux utility functionsimport{combineReducers}from'redux';// redux-undo higher-order reducerimportundoablefrom'redux-undo';
Then, addundoable to your reducer(s) like this:
combineReducers({counter:undoable(counter)})
Aconfiguration can be passed like this:
combineReducers({counter:undoable(counter,{limit:10// set a limit for the size of the history})})
When you expose an undo redo history action to your app users, you will not want those actionto apply on your whole redux state.Lets see this with naive document editor state.
constrootReducer=combineReducers({ui:uiReducer,document:documentReducer,})
wrapping the documentReducer with undoable higher order reducer
constrootReducer=combineReducers({ui:uiReducer,document:undoable(documentReducer),})
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, managedunder 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'twant to see your different history to undo/redo in sync.
Wrapping your reducer withundoable makes the state look like this:
{past:[...pastStatesHere...],present:{...currentStateHere...},future:[...futureStatesHere...]}
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.
Firstly, import the undo/redo action creators:
import{ActionCreators}from'redux-undo';
Then, you can usestore.dispatch() and the undo/redo action creators toperform 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
A configuration object can be passed toundoable() like this (values shownare 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 actionredoType:ActionTypes.REDO,// define a custom action type for this redo actionjumpType:ActionTypes.JUMP,// define custom action type for this jump actionjumpToPastType:ActionTypes.JUMP_TO_PAST,// define custom action type for this jumpToPast actionjumpToFutureType:ActionTypes.JUMP_TO_FUTURE,// define custom action type for this jumpToFuture actionclearHistoryType: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 reducersinitTypes:['@@redux-undo/INIT'],// history will be (re)set upon init action type// beware: those actions will not be passed down to the wrapped reducersdebug:false,// set to `true` to turn on debuggingignoreInitialState:false,// prevent user from undoing to the beginning, ex: client-side hydrationneverSkipReducer:false,// prevent undoable from skipping the reducer on undo/redo and clearHistoryType actionssyncFilter:false// set to `true` to synchronize the `_latestUnfiltered` state with `present` when an excluded action is dispatched})
Note: If you want to use just theinitTypes functionality, but not importthe whole redux-undo library, useredux-recycle!
You can use your redux store to set an initial history for your undoable reducers:
import{createStore}from'redux';constinitialHistory={past:[0,1,2,3],present:4,future:[5,6,7]}// Alternatively use the helper:// import { newHistory } from 'redux-undo';// const initialHistory = newHistory([0, 1, 2, 3], 4, [5, 6, 7]);conststore=createStore(undoable(counter),initialHistory);
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:[]}
If you want to group your actions together into single undo/redo steps, youcan add agroupBy function toundoable.redux-undo providesgroupByActionTypes as a basicgroupBy function:
importundoable,{groupByActionTypes}from'redux-undo';undoable(reducer,{groupBy:groupByActionTypes(SOME_ACTION)})// or with arraysundoable(reducer,{groupBy:groupByActionTypes([SOME_ACTION])})
In these cases, consecutiveSOME_ACTION actions will be considered a singlestep in the undo/redo history.
If you want to implement custom grouping behaviour, pass in your own functionwith the signature(action, currentState, previousHistory). If the returnvalue is notnull, 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, thenthe two states will be grouped together in one step.
If the return value isnull, thenredux-undo will not group the next statewith the previous state.
ThegroupByActionTypes 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.
WhengroupBy groups a state change, the associatedgroup will be savedalongsidepast,present, andfuture so that it may be referenced by thenext state change.
After an undo/redo/jump occurs, the current group gets reset tonull so thatthe undo/redo history is remembered.
If you don't want to include every action in the undo/redo history, you can addafilter function toundoable. This is useful for, for example, excludingactions that were not triggered by the user.
redux-undo provides you with theincludeAction andexcludeAction helpersfor basic filtering. They should be imported like this:
importundoable,{includeAction,excludeAction}from'redux-undo';
Now you can use the helper functions:
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: Sincebeta4,only actions resulting in a new state are recorded. This means the(now deprecated)distinctState() filter is auto-applied.
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}})
You can also use our helper to combine filters.
importundoable,{combineFilters}from'redux-undo'functionisActionSelfExcluded(action){returnaction.wouldLikeToBeInHistory}functionareWeRecording(action,state){returnstate.recording}undoable(reducer,{filter:combineFilters(isActionSelfExcluded,areWeRecording)})
When implementing a filter function, it only prevents the old state from beingstored in the history.filter does not prevent the present state from beingupdated.
If you want to ignore an action completely, as in, not even update the presentstate, you can make use ofredux-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)
Have a read of theImplementing Undo History recipe in the Redux documents, which explains in detail how redux-undo works.
If you have a question or just want to discuss something with other redux-undo users/maintainers,chat with the community on discord (discord.gg/GbHZTmd33n)!
Also, look at the documentation over atredux-undo.js.org.
MIT, seeLICENSE.md for more information.
About
♻️ higher order reducer to add undo/redo functionality to redux state containers
Topics
Resources
License
Contributing
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Packages0
Uh oh!
There was an error while loading.Please reload this page.
