Last active
November 30, 2023 05:00
-
-
Save pastelmind/8205beb5a302e7750cbf62bc6e7e36f2 to your computer and use it in GitHub Desktop.
Simple Valtio history store (alternative to proxyWithHistory)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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