Skip to content

Instantly share code, notes, and snippets.

@1j01
Last active October 14, 2024 01:43
Show Gist options
  • Select an option

  • Save 1j01/bd2329547904b97abc52fd5e76b008d8 to your computer and use it in GitHub Desktop.

Select an option

Save 1j01/bd2329547904b97abc52fd5e76b008d8 to your computer and use it in GitHub Desktop.
Undo/redo history pattern example in JavaScript
const undos = [];
const redos = [];
// usage example: undoable(()=> { state.width = 100; });
// (this also supports calling undoable with no action function, before
// continuously changing the state (e.g. adding points to a brush stroke),
// but this is not recommended; it would be better to, for instance,
// have a separate state for a brush stroke in progress,
// and add it to the main state as an undoable step when it's finished;
// this would also let you cancel the operation simply)
const undoable = (synchronousAction) => {
// you may also want to mark the document as unsaved here
// TODO: don't destroy history as is the common practice, but instead provide nonlinear history -
// keep a tree of states and show this tree to the user when they attempt to redo after undoing and doing something other than undo/redo;
// that way they can at least get back to any state, if not merge states after diverging, which could be complicated;
// also provide a way to show the tree at any time, perhaps ctrl+shift+y
redos.length = 0;
undos.push(getState());
if (synchronousAction) {
synchronousAction();
onStateChanged();
}
return true;
};
const undo = () => {
if (undos.length < 1) { return false; }
redos.push(getState());
setState(undos.pop());
return true;
};
const redo = () => {
if (redos.length < 1) { return false; }
undos.push(getState());
setState(redos.pop());
return true;
};
// --- now interface with the application ---
let state = {
// if you're going to save files based on the serialized state OR persist it in localStorage,
// you should include a version number, so old states can be upgraded,
// and the user can be told when old states aren't supported
// for an example of this pattern, see https://github.com/1j01/mopaint/blob/2db35857fbbb76980ce1229dadef531bf5147fc6/src/components/App.js#L60
// (that one TODO comment is accidentally left in there, it's done with `nounPhraseThingToLoad`)
// formatVersion: 1,
width: 640,
height: 480,
strokes: []
};
const onStateChanged = () => {
// could update any computed (derived) state here to ensure consistency when saving
// or better yet, keep computed state out of `state`!
// (derived state would be, for instance, if your app is a traffic simulator, and the user places roads,
// but buildings and trees are filled in automatically as decoration,
// if the buildings and trees are a function of the road structure, they are derived state)
// you should probably save the user's data:
/*
try {
localStorage.appState = getState();
} catch (error) {
// show a warning here that data isn't saved
// but don't use alert() or anything that obtrusive
// (the user should still be able to use the tool with site data turned off in their browser,
// but just be made aware that their data isn't safe (and why))
}
*/
render();
};
// serialize the state
const getState = () => JSON.stringify(state);
// deserialize and apply a state
const setState = (stateJSON) => {
state = JSON.parse(stateJSON);
onStateChanged();
};
const render = () => {
// TODO (render based on state)
};
/*
// load from localStorage
try {
if (localStorage.appState) {
setState(localStorage.appState);
}
} catch(err) { }
*/
render();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment