Skip to content

Instantly share code, notes, and snippets.

@pastelmind
Last active November 30, 2023 05:00
Show Gist options
  • Save pastelmind/8205beb5a302e7750cbf62bc6e7e36f2 to your computer and use it in GitHub Desktop.
Save pastelmind/8205beb5a302e7750cbf62bc6e7e36f2 to your computer and use it in GitHub Desktop.
Simple Valtio history store (alternative to proxyWithHistory)
// Replace ReadonlyDeep with Readonly if you don't want to use type-fest
import type { ReadonlyDeep } from 'type-fest';
import { proxy } from 'valtio';
/**
* Simple history-keeping store with undo/redo.
* You can also subscribe to the history itself
* (which isn't possible with `proxyWithHistory()`).
*/
interface HistoryStore<T extends object> {
/** Saved snapshots */
readonly snapshots: readonly ReadonlyDeep<T>[];
/**
* Current history index.
* Changing this also changes the {@link value}.
*/
index: number;
/** Current state */
readonly value: ReadonlyDeep<T>;
/** Pushes a new undo item into the history. */
setValue(
valueOrUpdater: ReadonlyDeep<T> | ((prev: T) => ReadonlyDeep<T>),
): void;
/** Indicates whether there are undo-able snapshots. */
readonly canUndo: boolean;
/** Indicates whethere there are redo-able snapshots */
readonly canRedo: boolean;
/**
* Decrements the history index.
* Does nothing if the index is at the start.
*/
undo(): void;
/**
* Increments the history index.
* Does nothing if the index is at the end.
*/
redo(): void;
/**
* Resets the history, discarding all snapshots.
* @param initialValue
*/
reset(initialValue: ReadonlyDeep<T>): void;
}
/** Creates a Valtio store with undo/redo. */
export function createHistoryStore<T extends object>(
initialValue: T,
): HistoryStore<T> {
const store = proxy({
snapshots: [initialValue] as ReadonlyDeep<T>[],
index_: 0,
get index() {
return this.index_;
},
set index(newIndex: number) {
if (!Number.isSafeInteger(newIndex)) {
throw new Error(`Index must be safe integer, got ${newIndex}`);
}
if (!(0 <= newIndex && newIndex < this.snapshots.length)) {
throw new Error(
`Index out of bounds: ${newIndex} (${this.snapshots.length} snapshot(s) in history)`,
);
}
this.index_ = newIndex;
},
get value() {
return this.snapshots[this.index];
},
setValue: (
valueOrUpdater: ReadonlyDeep<T> | ((prev: T) => ReadonlyDeep<T>),
) => {
store.snapshots.splice(
store.index + 1,
Number.POSITIVE_INFINITY,
typeof valueOrUpdater === 'function'
? valueOrUpdater(store.value as T)
: valueOrUpdater,
);
store.index++;
},
get canUndo() {
return this.index > 0;
},
get canRedo() {
return this.index < this.snapshots.length - 1;
},
undo: () => {
if (store.index > 0) {
store.index--;
}
},
redo: () => {
if (store.index < store.snapshots.length - 1) {
store.index++;
}
},
reset: (initialValue: ReadonlyDeep<T>) => {
store.index = 0;
store.snapshots = [initialValue] as ReadonlyDeep<T>[];
},
});
return store;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment