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 mementoshistoryPointer: number- Current position in historyoriginator: Originator- The originator instancecanUndo: ComputedValue<boolean>- Whether undo is possiblecanRedo: ComputedValue<boolean>- Whether redo is possiblesaveState(): void- Save current state to historyundo(): void- Restore previous stateredo(): void- Restore next statesubscribe(callback: (keys: PropertyKey[]) => void): () => void- Subscribe to caretaker changes
MementoBaseOriginator
getMemento(): TMemento | null- Get current state snapshotrestoreMemento(memento: TMemento): void- Restore state from memento
MementoDiffOriginator
getState(): TState- Get current stategetMemento(state: TState, prevState: TState | null): TMemento | null- Calculate diff between statesrestoreMemento(memento: TMemento, action: RestoreMementoAction): void- Restore state by applying diff