const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Create example action for talk'
}
// a typical redux action
from redux-undo's README.md:
const initialState = {
past: [],
present: null, // (?) How do we initialize the present?
future: []
}
function undoable(state = initialState, action) {
const { past, present, future } = state
switch (action.type) {
case 'UNDO':
const previous = past[past.length - 1]
const newPast = past.slice(0, past.length - 1)
return {
past: newPast,
present: previous,
future: [ present, ...future ]
}
case 'REDO':
const next = future[0]
const newFuture = future.slice(1)
return {
past: [ ...past, present ],
present: next,
future: newFuture
}
default:
// (?) How do we handle other actions?
return state
}
}
// `createLogger`, a higher-order function (that returns another function)
function createLogger(name) {
return function logger(message) {
// this function can access `name` from the upper scope!
console.log(name + ': ' + message)
}
}
const log = createLogger('redux-undo')
log('undo action received') // prints out `redux-undo: undo action received`
// `doNothingWith`, a higher-order reducer (that does nothing at all!)
function doNothingWith(reducer) {
return function (state, action) {
// Just call the passed reducer
return reducer(state, action)
}
}
// `combineReducers`, a higher-order reducer (that combines other reducers!)
function combineReducers(reducers) {
return function (state = {}, action) {
return Object.keys(reducers).reduce((nextState, key) => {
// Call every reducer with the part of the state it manages
nextState[key] = reducers[key](state[key], action)
return nextState
}, {})
}
}
This is a redux utility function: http://rackt.org/redux/docs/api/combineReducers.html
function undoable(reducer) {
// Call the reducer with empty action to populate the initial state
const initialState = {
past: [],
present: reducer(undefined, {}), // Problem 1 solved!
future: []
}
// Return a reducer that handles undo and redo
return function (state = initialState, action) {
const { past, present, future } = state
switch (action.type) {
case 'UNDO':
// ...
case 'REDO':
// ...
default:
// Delegate handling the action to the passed reducer
const newPresent = reducer(present, action) // Problem 2 solved!
if (present === newPresent) {
return state
}
return {
past: [ ...past, present ], // Problem 3 solved!
present: newPresent,
future: []
}
}
}
}
// This is a reducer
function todos(state = [], action) {
/* ... */
}
// This is also a reducer!
const undoableTodos = undoable(todos)
import { createStore } from 'redux'
const store = createStore(undoableTodos)
store.dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
})
store.dispatch({
type: 'ADD_TODO',
text: 'Implement Undo'
})
store.dispatch({
type: 'UNDO'
})
npm install --save redux-undo
npm install --save redux-undo@beta
import { combineReducers } from 'redux' // Redux utility functions
import undoable from 'redux-undo' // redux-undo higher-order reducer
const reducers = combineReducers({
counter: undoable(counter, {
limit: 10 // set a limit for the history
})
})
{
todos: [...],
filter: 'active'
}
{
past: [...pastStatesHere...],
present: {
todos: [...],
filter: 'active'
},
future: [...futureStatesHere...]
}
From:
reducer
To:
undoable(reducer)
import { ActionCreators } from 'redux-undo'
store.dispatch(ActionCreators.undo()) // undo the last action
store.dispatch(ActionCreators.redo()) // redo the last action
// jump to requested index in the past[] array
store.dispatch(ActionCreators.jumpToPast(index))
// jump to requested index in the future[] array
store.dispatch(ActionCreators.jumpToFuture(index))
undoable(reducer, {
filter: function filterActions(action, currentState, previousState) {
// only add to history if action is SOME_ACTION
return action.type === SOME_ACTION
}
})
// or you could do...
undoable(reducer, {
filter: function filterState(action, currentState, previousState) {
// only add to history if state changed
return currentState !== previousState
}
})
// or with helpers...
import undoable, { distinctState, includeAction, excludeAction } from 'redux-undo'
undoable(reducer, { filter: includeAction(SOME_ACTION) })
undoable(reducer, { filter: excludeAction(SOME_ACTION) })
undoable(reducer, { filter: includeAction([SOME_ACTION, SOME_OTHER_ACTION]) })
undoable(reducer, { filter: excludeAction([SOME_ACTION, SOME_OTHER_ACTION]) })
undoable(reducer, { filter: distinctState() })
Making an existing app (todos) undoable (todos-with-undo)
with redux-undo
Check out the result: rackt/redux > examples/todos-with-undo
I hope you learned something useful today!