redux-undo
:recycle: higher order reducer to add undo/redo functionality to redux state containers
Top Related Projects
Flux Standard Action utilities for Redux.
The official, opinionated, batteries-included toolset for efficient Redux development
persist and rehydrate a redux store
Logger for Redux
Quick Overview
The redux-undo
library provides a higher-order reducer that adds undo/redo functionality to a Redux application. It allows developers to easily add undo/redo capabilities to their Redux-based applications, without having to implement the complex logic themselves.
Pros
- Simplifies Undo/Redo Implementation: The library abstracts away the complex state management required for implementing undo/redo functionality, allowing developers to focus on building their application.
- Flexible Configuration:
redux-undo
provides a wide range of configuration options, allowing developers to customize the undo/redo behavior to fit their specific use case. - Supports Async Actions: The library is designed to work seamlessly with asynchronous actions, making it suitable for a wide range of Redux-based applications.
- Actively Maintained: The project is actively maintained, with regular updates and bug fixes, ensuring its continued reliability and compatibility with the latest versions of Redux.
Cons
- Increased Complexity: Integrating
redux-undo
into an existing Redux application can add some complexity to the codebase, as developers need to understand how to configure and use the library effectively. - Performance Considerations: Storing the entire state history can lead to performance issues, especially in applications with large state trees or frequent state changes.
- Potential Compatibility Issues: As with any third-party library, there is a risk of compatibility issues with other Redux-related libraries or future versions of Redux itself.
- Limited Undo Depth: The library's default configuration only stores a limited number of past states, which may not be sufficient for all use cases.
Code Examples
Here are a few examples of how to use the redux-undo
library in a Redux application:
- Basic Setup:
import { createStore, combineReducers } from 'redux';
import undoable, { distinctState } from 'redux-undo';
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
const rootReducer = combineReducers({
counter: undoable(counterReducer, {
filter: distinctState(),
}),
});
const store = createStore(rootReducer);
- Undo/Redo Actions:
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'UNDO' }); // Undo the last action
store.dispatch({ type: 'REDO' }); // Redo the last undone action
- Custom Action Types:
const { ActionCreators } = require('redux-undo');
store.dispatch(ActionCreators.undo());
store.dispatch(ActionCreators.redo());
store.dispatch(ActionCreators.clearHistory());
- Async Actions:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import undoable from 'redux-undo';
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
const rootReducer = combineReducers({
counter: undoable(counterReducer),
});
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
store.dispatch((dispatch) => {
dispatch({ type: 'INCREMENT' });
dispatch({ type: 'DECREMENT' });
});
Getting Started
To get started with redux-undo
, follow these steps:
- Install the library using npm or yarn:
npm install redux-undo
- Import the
undoable
higher-order reducer and use it to wrap your
Competitor Comparisons
Flux Standard Action utilities for Redux.
Pros of redux-actions
- Simplifies action creation and handling with utility functions
- Reduces boilerplate code for Redux actions and reducers
- Supports Flux Standard Action (FSA) format out of the box
Cons of redux-actions
- Focuses solely on action creation, lacking undo/redo functionality
- May require additional setup for complex action structures
- Does not provide built-in state history management
Code Comparison
redux-actions:
import { createAction, handleAction } from 'redux-actions';
const increment = createAction('INCREMENT');
const reducer = handleAction(increment, (state, action) => state + 1, 0);
redux-undo:
import undoable from 'redux-undo';
const reducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
};
const undoableReducer = undoable(reducer);
Key Differences
- redux-actions focuses on simplifying action creation and handling
- redux-undo provides undo/redo functionality for existing reducers
- redux-actions uses utility functions, while redux-undo wraps reducers
- redux-undo maintains state history, redux-actions does not
- redux-actions adheres to FSA, while redux-undo is more flexible in action structure
The official, opinionated, batteries-included toolset for efficient Redux development
Pros of Redux Toolkit
- Provides a comprehensive set of tools for efficient Redux development
- Simplifies common Redux use cases with utilities like
createSlice
- Includes built-in support for immutable updates and async logic
Cons of Redux Toolkit
- Larger bundle size due to additional features and dependencies
- Steeper learning curve for developers new to Redux concepts
- May introduce unnecessary complexity for smaller projects
Code Comparison
Redux Toolkit:
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: state => state + 1,
decrement: state => state - 1
}
})
Redux Undo:
import undoable from 'redux-undo'
const reducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT': return state + 1
case 'DECREMENT': return state - 1
default: return state
}
}
const undoableReducer = undoable(reducer)
Redux Undo focuses specifically on adding undo/redo functionality to Redux applications, while Redux Toolkit aims to provide a more comprehensive set of tools for Redux development. Redux Toolkit offers a wider range of features and utilities, but may be overkill for simpler projects or those primarily concerned with undo/redo functionality.
persist and rehydrate a redux store
Pros of redux-persist
- Focuses on state persistence, allowing for seamless data storage and retrieval
- Supports multiple storage engines (e.g., localStorage, AsyncStorage)
- Offers flexible configuration options for selective persistence
Cons of redux-persist
- Limited to persistence functionality, lacking undo/redo capabilities
- May require additional setup and configuration compared to simpler solutions
- Potential performance impact when persisting large amounts of data
Code Comparison
redux-persist:
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
const persistConfig = {
key: 'root',
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
redux-undo:
import undoable from 'redux-undo'
const undoableReducer = undoable(rootReducer)
Key Differences
- Functionality: redux-persist focuses on state persistence, while redux-undo provides undo/redo functionality.
- Complexity: redux-persist requires more setup but offers more flexibility, whereas redux-undo is simpler to implement.
- Use case: Choose redux-persist for maintaining state across sessions, and redux-undo for implementing history-based features.
Conclusion
Both libraries serve different purposes within the Redux ecosystem. redux-persist is ideal for applications requiring state persistence, while redux-undo is better suited for implementing undo/redo functionality. The choice between the two depends on the specific requirements of your project.
Logger for Redux
Pros of redux-logger
- Provides detailed logging of Redux actions and state changes
- Offers customizable logging options, including filtering and formatting
- Supports logging in various environments (browser console, Node.js)
Cons of redux-logger
- Focuses solely on logging, lacking undo/redo functionality
- Can potentially impact performance if used excessively in production
- Requires manual setup and configuration for optimal use
Code Comparison
redux-logger:
import { createLogger } from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
applyMiddleware(logger)
);
redux-undo:
import undoable from 'redux-undo';
const reducer = undoable(yourReducer);
const store = createStore(reducer);
Key Differences
- Purpose: redux-logger is for debugging and monitoring Redux actions, while redux-undo provides undo/redo functionality
- Implementation: redux-logger is middleware, redux-undo wraps reducers
- Functionality: redux-logger focuses on logging, redux-undo on state management
- Performance impact: redux-logger may affect performance more in production environments
Use Cases
- redux-logger: Debugging, development, and monitoring Redux applications
- redux-undo: Implementing undo/redo features in Redux-based applications
Both libraries serve different purposes and can be used together in a Redux application to enhance development and user experience.
Convert designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual CopilotREADME
redux undo/redo
simple undo/redo functionality for redux state containers
Protip: Check out the todos-with-undo example or the redux-undo-boilerplate to quickly get started with redux-undo
.
Switching from 0.x to 1.0: Make sure to update your programs to the latest History API.
Help wanted: We are looking for volunteers to maintain this project, if you are interested, feel free to contact me at me@omnidan.net
This README is about the new 1.0 branch of redux-undo, if you are using
or plan on using 0.6, check out the 0.6
branch
Note on Imports
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:
import ReduxUndo from 'redux-undo';
We are also supporting UMD build:
var ReduxUndo = window.ReduxUndo.default;
once again .default
is required.
Installation
npm install --save redux-undo
API
import undoable from 'redux-undo';
undoable(reducer)
undoable(reducer, config)
Making your reducers undoable
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.present.counter
after wrapping your reducer with undoable
.
To install, firstly import redux-undo
:
// Redux utility functions
import { combineReducers } from 'redux';
// redux-undo higher-order reducer
import undoable from 'redux-undo';
Then, add undoable
to your reducer(s) like this:
combineReducers({
counter: undoable(counter)
})
A configuration can be passed like this:
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.
const rootReducer = combineReducers({
ui: uiReducer,
document: documentReducer,
})
wrapping the documentReducer with undoable higher order reducer
const rootReducer = 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, managed under redux-undo.
const rootReducer = 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:
{
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.
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 action
store.dispatch(ActionCreators.redo()) // redo the last action
store.dispatch(ActionCreators.jump(-2)) // undo 2 steps
store.dispatch(ActionCreators.jump(5)) // redo 5 steps
store.dispatch(ActionCreators.jumpToPast(index)) // jump to requested index in the past[] array
store.dispatch(ActionCreators.jumpToFuture(index)) // jump to requested index in the future[] array
store.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 history
filter: () => 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:
import { createStore } from 'redux';
const initialHistory = {
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]);
const store = 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';
const store = 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 arrays
undoable(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:
import undoable, { 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: 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: function filterActions(action, currentState, previousHistory) {
return action.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: function filterState(action, currentState, previousHistory) {
let { past, present, future } = previousHistory;
return future.length === 0; // only add to history if future is empty
}
})
Combining Filters
You can also use our helper to combine filters.
import undoable, {combineFilters} from 'redux-undo'
function isActionSelfExcluded(action) {
return action.wouldLikeToBeInHistory
}
function areWeRecording(action, state) {
return state.recording
}
undoable(reducer, {
filter: combineFilters(isActionSelfExcluded, areWeRecording)
})
Ignoring Actions
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
)
What is this magic? How does it work?
Have a read of the Implementing Undo History recipe in the Redux documents, which explains in detail how redux-undo works.
Chat / Support
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 at redux-undo.js.org.
Sponsors
- Thanks to @tomaAlex (https://woggo.ro/) for sponsoring my projects!
License
MIT, see LICENSE.md
for more information.
Top Related Projects
Flux Standard Action utilities for Redux.
The official, opinionated, batteries-included toolset for efficient Redux development
persist and rehydrate a redux store
Logger for Redux
Convert designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual Copilot