• Stars
    star
    2,898
  • Rank 15,003 (Top 0.4 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 8 years ago
  • Updated 9 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

โ™ป๏ธ higher order reducer to add undo/redo functionality to redux state containers

redux undo/redo

NPM version (>=1.0) NPM Downloads Coverage Status Dependencies js-standard-style GitHub license

simple undo/redo functionality for redux state containers

https://i.imgur.com/M2KR4uo.gif

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 [email protected]


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

License

MIT, see LICENSE.md for more information.

More Repositories

1

node-emoji

๐Ÿ’– simple emoji support for node.js projects
TypeScript
1,270
star
2

redux-ignore

โ™ป๏ธ higher-order reducer to ignore redux actions
JavaScript
843
star
3

redux-recycle

โ™ป๏ธ higher-order reducer to reset the redux state on certain actions
JavaScript
147
star
4

redux-undo-boilerplate

โ™ป๏ธ ๐Ÿ”ง a magical boilerplate with hot reloading and awesome error handlingโ„ข
JavaScript
121
star
5

ArduinoPure

๐Ÿ“ผ This is a project to achieve compiling programs for the Arduino without any external programs like the Arduino IDE. It uses all the standard programs you love so much: make, cc, ... This is also the only way I was able to flash arduino programs to microcontrollers with the stk500.
C++
14
star
6

asv

โญ Electron / Atom Shell Version Management ร  la "n" and "m"
Shell
9
star
7

git-leaf

๐Ÿƒ simple and beautiful git trees
JavaScript
8
star
8

python-latex

๐ŸŽ“ Python modules for LaTeX parsing and management
Python
7
star
9

node-np

๐ŸŽต standalone last.fm bot written in node.js
JavaScript
7
star
10

node-kaomoji

( อกยฐ อœส– อกยฐ) simple kaomoji support for node.js projects
HTML
6
star
11

statsbot

(telegram) message stats bot
JavaScript
5
star
12

now

โœจ simple real-time reddit/HN-inspired social network crafted with React and Meteor
JavaScript
4
star
13

iForceBot

[DISCONTINUED] If you want to work on it, feel free to fork and start working. An IRC bot with much power and easy-to-code modules! Daniel0108 only maintains the framework and *his* plugins. The plugin downloader and repository are not official, nor maintained by him.
Python
4
star
14

ZenHubPlus

Userstyle to make ZenHub features look more integrated on GitHub
CSS
3
star
15

log-simple

๐Ÿ’ฌ Super Simple JavaScript Logging
JavaScript
3
star
16

karma

manage activities with a points/karma system
JavaScript
3
star
17

coffeedonate

โ˜• donate a cup of coffee, because coffee == <3
JavaScript
3
star
18

tuio-to-tlay

Converting TUIO to TLay Touch events
C++
2
star
19

PyTerm

๐Ÿ’ป Yet-another simple Python Terminal simulator made to practise curses in python.
Python
2
star
20

omnidan.github.io

๐ŸŒ my website
HTML
2
star
21

tlay-touch

TLay Touch is a UDP protocol that allows you to add multitouch support to programs.
C++
2
star
22

scipy-notebook-0.18

scipy-notebook with scikit-learn 0.18
1
star
23

Arrbus

๐ŸšŒ Communication with the BusPirate v3
JavaScript
1
star
24

meilog

๐Ÿ“ƒ beautiful logging library with support for structured logs (for cloud services) and local logging with colors
JavaScript
1
star
25

tn2_manager

The manager system for the new TouchNet 2.X
C++
1
star
26

dotfiles-old

๐Ÿ’พ My dotfiles, mostly used to share my configuration on all my machines, but of course you can also use them.
Vim Script
1
star
27

tn2_core

The core system for the new TouchNet 2.X
C++
1
star
28

omnidan-net

omnidan.net website
HTML
1
star
29

reddit-roulette

๐ŸŽช Chrome extension: Easily get to a random subreddit by clicking a button. Wasting time has never been so easy.
JavaScript
1
star
30

sync

TouchLay Sync - Real-time document-based data syncronisation service in NodeJS using socket.io and MongoDB.
JavaScript
1
star
31

BRUTE.PY

๐Ÿ”ง [DISCONTINUED] If you want to work on it, feel free to fork and start working. Simple python bruteforcing script for educational and testing purposes.
Python
1
star
32

Shift

simple shift-based time scheduling for busy people
CSS
1
star