Background
For a while now I've been wracking my brain as to how you would implement undo / redo in Redux with server interactions (via ajax).
I've come up with a solution using a command pattern where actions are registered with an execute
and undo
method as Commands, and instead of dispatching actions you dispatch commands. The commands are then stored in a stack and raise new actions where required.
My current implementation uses middleware to intercept dispatches, test for Commands and call methods of the Command and looks something like this:
Middleware
let commands = [];
function undoMiddleware({ dispatch, getState }) {
return function (next) {
return function (action) {
if (action instanceof Command) {
// Execute the command
const promise = action.execute(action.value);
commands.push(action);
return promise(dispatch, getState);
} else {
if (action.type === UNDO) {
// Call the previous commands undo method
const command = commands.pop();
const promise = command.undo(command.value);
return promise(dispatch, getState);
} else {
return next(action);
}
}
};
};
}
Actions
const UNDO = 'UNDO';
function undo() {
return {
type: UNDO
}
}
function add(value) {
return (dispatch, getState) => {
const { counter } = getState();
const newValue = counter + value;
return new Promise((resolve, reject) => {
resolve(newValue); // Ajax call goes here
}).then((data) => {
dispatch(receiveUpdate(data));
});
}
}
function sub(value) {
return (dispatch, getState) => {
const { counter } = getState();
const newValue = counter - value;
return new Promise((resolve, reject) => {
resolve(newValue); // Ajax call goes here
}).then((data) => {
dispatch(receiveUpdate(data));
});
}
}
Commands
class Command {
execute() {
throw new Error('Not Implemented');
}
undo() {
throw new Error('Not Implemented');
}
}
class AddCommand extends Command {
constructor(value) {
super();
this.value = value;
}
execute() {
return add(this.value);
}
undo() {
return sub(this.value);
}
}
App
const store = createStoreWithMiddleware(appReducer);
store.dispatch(new AddCommand(10)); // counter = 10
store.dispatch(new AddCommand(5)); // counter = 15
// Some time later
store.dispatch(undo()); // counter = 10
(a more complete example here)
There are several issues I've found with my current approach:
- Due to implementing via middleware, only one stack may exist for the entire application.
- Cannot customise
UNDO
command type. - Creating a Command to call actions which in turn return promises seems very convoluted.
- Commands are added to the stack before the action completes. What happens for errors?
- As commands are not in state, cannot add is_undoable functionality.
- How would you implement optimistic updates?
Help
My question then, is can anyone suggest a better way of implementing this functionality within Redux?
The biggest flaws I see right now are the commands being added before actions have completed, and how it would be difficult to add optimistic updates to the mix.
Any insight is appreciated.