You crop a photo, rotate it a little, adjust contrast, and add a black-and-white filter. Then the user presses Undo.
The tempting fix is to keep an undo stack that reaches into the photo editor, reads every field, and puts the old values back. That works for about five minutes. Then the editor changes shape, the undo code knows too much, and a small refactor turns into a history bug.
The Memento pattern keeps that boundary cleaner. The photo editor creates snapshots of its edit settings. The undo stack stores those snapshots. It does not know what is inside.
The usual approach
Without a pattern, undo history often starts as an array of plain objects:
history.push({
crop: editor.crop,
rotation: editor.rotation,
adjustments: editor.adjustments,
filter: editor.filter,
annotations: editor.annotations,
});This works only while the photo editor stays simple. The problem is that the undo code now depends on the editor's internal details. It must know that crop is stored in editor.crop, rotation in editor.rotation, filters in editor.filter, and annotations in editor.annotations.
That means two parts of the app now need to change whenever the editor changes: the editor itself, and the undo code. If annotations later move into a layer system, or filters become a list of operations, undo can break even though the user-facing feature is the same.
There is another trap: saving too much. A photo editor could save a full copy of the image after every edit, but that gets expensive quickly. Most undo steps only need the edit settings: crop, rotation, brightness, filter, and annotations.
The pain point is ownership. The editor should decide what needs to be saved. The undo system should only store and return those saved snapshots.

The pattern in one minute
Memento usually gets explained with three names:
- Originator: the object that owns the state. Here, that is a photo editor.
- Memento: a snapshot of that state.
- Caretaker: the thing that stores snapshots and asks for undo or redo.
Refactoring.Guru describes Memento as a behavioral pattern that saves and restores an object's previous state without exposing implementation details: Memento pattern.
That privacy boundary is the useful part. Undo history should not care whether the editor stores crop data, filters, and annotations as objects, maps, command lists, or something we have not invented yet.

Build a small photo editor
The editor does not store a new copy of the image after every edit. That would get expensive fast. It stores the recipe needed to render the current version: crop preset, rotation, adjustments, filter, and annotations.
class PhotoEditor {
#imageId;
#crop = "original";
#rotation = 0;
#adjustments = {
brightness: 0,
contrast: 0,
saturation: 0,
};
#filter = "none";
#annotations = [];
constructor(imageId) {
this.#imageId = imageId;
}
setCrop(crop) {
this.#crop = crop;
}
setRotation(rotation) {
this.#rotation = rotation;
}
setAdjustment(name, value) {
this.#adjustments = {
...this.#adjustments,
[name]: value,
};
}
setFilter(filter) {
this.#filter = filter;
}
addAnnotation(value) {
if (value.trim() === "") {
return;
}
this.#annotations = [
...this.#annotations,
{
type: "text",
value: value.trim(),
x: 120,
y: 80,
},
];
}
createSnapshot() {
return new PhotoEditSnapshot({
imageId: this.#imageId,
crop: this.#crop,
rotation: this.#rotation,
adjustments: this.#adjustments,
filter: this.#filter,
annotations: this.#annotations,
});
}
restore(snapshot) {
const state = snapshot.getState();
this.#imageId = state.imageId;
this.#crop = state.crop;
this.#rotation = state.rotation;
this.#adjustments = state.adjustments;
this.#filter = state.filter;
this.#annotations = state.annotations;
}
getState() {
return {
imageId: this.#imageId,
crop: this.#crop,
rotation: this.#rotation,
adjustments: this.#adjustments,
filter: this.#filter,
annotations: this.#annotations,
};
}
}PhotoEditor is the Originator. It decides what belongs in a snapshot. The history object never reads #crop, #filter, or #annotations directly.
Private class fields fit this example well because JavaScript only allows access from inside the class declaration: MDN private elements.
Make snapshots immutable
A snapshot should be boring. Once it exists, it should not change. If old history can be mutated through a shared reference, undo becomes guesswork.
class PhotoEditSnapshot {
#state;
#createdAt;
constructor(state) {
this.#state = deepFreeze(structuredClone(state));
this.#createdAt = new Date();
}
getState() {
return structuredClone(this.#state);
}
getLabel() {
return this.#createdAt.toISOString();
}
}
function deepFreeze(value, seen = new WeakSet()) {
if (value === null || typeof value !== "object") {
return value;
}
if (seen.has(value)) {
return value;
}
seen.add(value);
Object.freeze(value);
for (const child of Object.values(value)) {
deepFreeze(child, seen);
}
return value;
}There are two protections here:
structuredClone()breaks shared references when the snapshot is created and when it is read.deepFreeze()prevents mutation inside the stored snapshot.
structuredClone() creates a deep copy using the structured clone algorithm: MDN structuredClone. The algorithm can handle cycles by tracking visited references, which matters once your state is more than plain nested objects: MDN structured clone algorithm.
It is still a copying API, not a save-anything API. It does not clone functions or every host object. Keep snapshots focused on data, not behavior.
Store history with undo and redo stacks
The Caretaker stores snapshots and hands them back later. It does not inspect or change their contents.
class HistoryManager {
#undoStack = [];
#redoStack = [];
#capacity;
constructor(capacity = 100) {
this.#capacity = capacity;
}
commit(snapshot) {
this.#undoStack.push(snapshot);
this.#redoStack = [];
if (this.#undoStack.length > this.#capacity) {
this.#undoStack.shift();
}
}
undo() {
if (this.#undoStack.length < 2) {
return null;
}
const current = this.#undoStack.pop();
this.#redoStack.push(current);
return this.#undoStack[this.#undoStack.length - 1];
}
redo() {
const snapshot = this.#redoStack.pop();
if (!snapshot) {
return null;
}
this.#undoStack.push(snapshot);
return snapshot;
}
getCounts() {
return {
undo: this.#undoStack.length,
redo: this.#redoStack.length,
};
}
}The stack behavior is small enough to keep in your head:
commit()saves a new current state and clears redo history.undo()moves the current snapshot to the redo stack and returns the previous snapshot.redo()moves a redo snapshot back to the undo stack.
push() and pop() are constant-time operations. The odd one out is shift(), which removes the oldest snapshot after the history reaches capacity. That is fine for a small limit. If you expect thousands of snapshots, use a ring buffer instead.
Put it together
Now the editor can move backward and forward through edit recipes.
const editor = new PhotoEditor("beach-trip.jpg");
const history = new HistoryManager(30);
history.commit(editor.createSnapshot());
editor.setCrop("square");
history.commit(editor.createSnapshot());
editor.setRotation(-2);
editor.setAdjustment("brightness", 8);
editor.setAdjustment("contrast", 12);
history.commit(editor.createSnapshot());
editor.setFilter("black-and-white");
editor.addAnnotation("Summer 2026");
history.commit(editor.createSnapshot());
console.log(editor.getState());
// filter: "black-and-white", annotations: 1
const previous = history.undo();
if (previous) {
editor.restore(previous);
}
console.log(editor.getState());
// filter: "none", annotations: 0
const older = history.undo();
if (older) {
editor.restore(older);
}
console.log(editor.getState());
// crop: "square", rotation: 0
const redone = history.redo();
if (redone) {
editor.restore(redone);
}
console.log(editor.getState());
// rotation: -2, contrast: 12The history manager is deliberately dull. It never reads editor fields. It never builds edit recipes. It only stores snapshots the editor created.
JSON versus structuredClone
Many JavaScript examples use JSON as the snapshot format:
class JsonSnapshot {
constructor(state) {
this.state = JSON.stringify(state);
}
getState() {
return JSON.parse(this.state);
}
}That is fine when state is plain data: strings, numbers, booleans, arrays, and simple objects.
It gets awkward once state contains values JSON does not represent well. Date values become strings. undefined, functions, and symbols are omitted or converted depending on where they appear. Cyclic references throw because JSON.stringify() does not resolve object cycles: MDN JSON.stringify, MDN cyclic object value.
For edit settings, form state, canvas metadata, or nested application state, I would usually start with structuredClone(). Persistence is a separate question. If snapshots need to survive a reload, make the snapshot shape explicitly serializable and treat JSON conversion as a storage boundary.
When not to use Memento
Memento works best when an object can make useful snapshots of its own state. Sometimes that is not the problem you actually have.
Use another approach when:
Full snapshots on every small change will waste memory if state is huge. In a photo editor, that means you should not copy raw image pixels into every memento.
The Command pattern may fit better when changes are easier to reverse than state is to copy. Event sourcing is better when you need an audit trail of what happened. Diffs are cheaper when most changes touch a small part of a large state object. And if restoration has to load from IndexedDB or a server, you now have an async API to design.
A real photo editor often mixes these ideas. It might debounce snapshot creation, store a full edit recipe every few changes, keep diffs between them, and cap history by memory size instead of item count.
Final version
Use Memento when you want undo and redo without making history code depend on object internals.
The photo editor owns state capture. The snapshot protects the saved edit recipe. The history manager stores the snapshots and stays out of the editor's private fields. That is the whole trick.
