Skip to main content
Version: v1.0

Memento Pattern

The Memento pattern allows you to save and restore the state of an object, enabling undo/redo functionality. use-less-react provides two implementations:

  • Base Memento: Stores complete snapshots of state
  • Diff Memento: Stores only the differences (deltas) between states, which is a bit more complex, but more memory-efficient

Base Memento

The base memento implementation stores complete state snapshots. Use this when the state is relatively small and simplicity of implementation is required.

Example: Text Editor

import {
MementoBaseCaretaker,
type MementoBaseOriginator,
ReactiveValue,
type ExtractReactiveValue,
ReactiveStore,
} from "@dxbox/use-less-react/classes";

export interface TextEditorSnapshot {
text: string;
format: string;
}

interface TextEditorStore {
text: ReactiveValue<string>;
format: ReactiveValue<string>;
}

const createTextEditorStore = (): TextEditorStore => ({
text: new ReactiveValue(""),
format: new ReactiveValue(""),
});

export class TextEditorOriginator
implements MementoBaseOriginator<TextEditorSnapshot>
{
private readonly _store: ReactiveStore<TextEditorStore>;

constructor() {
this._store = new ReactiveStore(createTextEditorStore());
}

setValue<K extends keyof TextEditorStore>(
key: K,
value: ExtractReactiveValue<TextEditorStore[K]>,
) {
this._store.values[key].set(value);
}

getValue<K extends keyof TextEditorStore>(key: K) {
return this._store.values[key].get();
}

get store() {
return this._store;
}

getMemento(): TextEditorSnapshot | null {
return this._store.toPlainObject();
}

public restoreMemento(memento: TextEditorSnapshot): void {
this.setValue("text", memento.text);
this.setValue("format", memento.format);
}
}

export class TextEditorCaretaker extends MementoBaseCaretaker<
TextEditorStore,
TextEditorSnapshot,
TextEditorOriginator
> {
constructor(originator: TextEditorOriginator) {
super(originator);
}
}

Usage

const originator = new TextEditorOriginator();
const caretaker = new TextEditorCaretaker(originator);

// Make changes
originator.setValue("text", "Hello");
originator.setValue("format", "txt");
caretaker.saveState(); // Save current state

originator.setValue("text", "Hello World");
caretaker.saveState(); // Save another state

// Undo
caretaker.undo(); // Restores to "Hello" with "txt" format

// Redo
caretaker.redo(); // Restores to "Hello World"

// Check if undo/redo is possible
const canUndo = caretaker.canUndo.get(); // ComputedValue<boolean>
const canRedo = caretaker.canRedo.get(); // ComputedValue<boolean>

Diff Memento

The diff memento implementation stores only the differences between states, making it more memory-efficient for large state objects or frequent state changes.

Example: User Profile with jsondiffpatch

This example shows how to use jsondiffpatch to efficiently track changes in large ReactiveObject states:

import {
ReactiveObject,
MementoDiffCaretaker,
type MementoDiffOriginator,
RestoreMementoAction,
} from "@dxbox/use-less-react/classes";
import jsondiffpatch from "jsondiffpatch";

export interface UserProfileState {
name: string;
email: string;
preferences: {
theme: "light" | "dark";
language: string;
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
};
metadata: {
createdAt: string;
lastLogin: string;
tags: string[];
};
}

// The delta type is the diff format from jsondiffpatch
export type UserProfileDelta = ReturnType<typeof jsondiffpatch.diff>;

interface UserProfileStore {
profile: ReactiveObject<UserProfileState>;
}

const createUserProfileStore = (): UserProfileStore => ({
profile: new ReactiveObject<UserProfileState>({
name: "",
email: "",
preferences: {
theme: "light",
language: "en",
notifications: {
email: false,
push: false,
sms: false,
},
},
metadata: {
createdAt: new Date().toISOString(),
lastLogin: new Date().toISOString(),
tags: [],
},
}),
});

export class UserProfileOriginator
implements MementoDiffOriginator<UserProfileState, UserProfileDelta>
{
private readonly store: UserProfileStore;
private readonly differ: ReturnType<typeof jsondiffpatch.create>;

constructor() {
this.store = createUserProfileStore();
this.differ = jsondiffpatch.create();
}

get profile(): ReactiveObject<UserProfileState> {
return this.store.profile;
}

getState(): UserProfileState {
return this.store.profile.get();
}

getMemento(
state: UserProfileState,
prevState: UserProfileState | null,
): UserProfileDelta | null {
if (!prevState) {
// First state: save the full state as initial diff
return this.differ.diff({}, state);
}

const delta = this.differ.diff(prevState, state);

// Return null if there are no differences
if (!delta || Object.keys(delta).length === 0) {
return null;
}

return delta;
}

restoreMemento(
memento: UserProfileDelta,
action: RestoreMementoAction,
): void {
const currentState = this.getState();

if (action === RestoreMementoAction.Undo) {
// Undo: unpatch the current state to get the previous state
const previousState = this.differ.unpatch(currentState, memento);
if (previousState) {
this.store.profile.set(previousState);
}
} else {
// Redo: patch the current state to get the next state
const nextState = this.differ.patch(currentState, memento);
if (nextState) {
this.store.profile.set(nextState);
}
}
}
}

export class UserProfileCaretaker extends MementoDiffCaretaker<
UserProfileStore,
UserProfileState,
UserProfileOriginator,
UserProfileDelta
> {
constructor(originator: UserProfileOriginator) {
super(originator);
}
}

Usage

const originator = new UserProfileOriginator();
const caretaker = new UserProfileCaretaker(originator);

// Make changes to the profile
originator.profile.set((draft) => {
draft.name = "John Doe";
draft.email = "john@example.com";
draft.preferences.theme = "dark";
draft.preferences.notifications.email = true;
draft.metadata.tags.push("premium");
});
caretaker.saveState(); // Saves only the differences

// Make more changes
originator.profile.set((draft) => {
draft.preferences.language = "it";
draft.preferences.notifications.push = true;
draft.metadata.lastLogin = new Date().toISOString();
});
caretaker.saveState(); // Saves only the new differences

// Undo
caretaker.undo(); // Restores to previous state (removes language, push notification, and lastLogin changes)

// Redo
caretaker.redo(); // Reapplies the changes

This approach is particularly useful for large state objects where storing full snapshots would be memory-intensive. The diff format from jsondiffpatch efficiently represents only what changed between states.

API

MementoBaseCaretaker / MementoDiffCaretaker

  • history: TMemento[] - Array of saved mementos
  • historyPointer: number - Current position in history
  • originator: Originator - The originator instance
  • canUndo: ComputedValue<boolean> - Whether undo is possible
  • canRedo: ComputedValue<boolean> - Whether redo is possible
  • saveState(): void - Save current state to history
  • undo(): void - Restore previous state
  • redo(): void - Restore next state
  • subscribe(callback: (keys: PropertyKey[]) => void): () => void - Subscribe to caretaker changes

MementoBaseOriginator

  • getMemento(): TMemento | null - Get current state snapshot
  • restoreMemento(memento: TMemento): void - Restore state from memento

MementoDiffOriginator

  • getState(): TState - Get current state
  • getMemento(state: TState, prevState: TState | null): TMemento | null - Calculate diff between states
  • restoreMemento(memento: TMemento, action: RestoreMementoAction): void - Restore state by applying diff