Skip to content

Instantly share code, notes, and snippets.

@mary-ext
Created August 2, 2025 06:39
Show Gist options
  • Save mary-ext/34f81d2a533fc2c2ec278a44b909fad0 to your computer and use it in GitHub Desktop.
Save mary-ext/34f81d2a533fc2c2ec278a44b909fad0 to your computer and use it in GitHub Desktop.
import { nanoid } from 'nanoid';
import { computed, onScopeDispose, shallowRef } from 'vue';
import { isNavigationFailure, useRouter } from 'vue-router';
import { defineStore } from '../stores';
export interface HistoryEntry {
id: string;
}
export const useHistoryStack = defineStore('history-stack', () => {
const router = useRouter();
const history = router.options.history;
const stack = shallowRef<(HistoryEntry | null)[]>(
(() => {
const length = history.state.position;
return arr(length, (idx) => (idx === length - 1 ? createEntry() : null));
})(),
);
const currentIndex = computed((): number => {
router.currentRoute.value;
return history.state.position - 1;
});
const currentEntry = computed((): HistoryEntry => {
return stack.value[currentIndex.value]!;
});
let navigation: 'push' | 'replace' | 'pop' | undefined;
{
const originalPush = router.push;
const originalReplace = router.replace;
router.push = function (...args) {
navigation = 'push';
return originalPush.apply(this, args);
};
router.replace = function (...args) {
navigation = 'replace';
return originalReplace.apply(this, args);
};
if (import.meta.hot) {
onScopeDispose(() => {
router.push = originalPush;
router.replace = originalReplace;
});
}
}
{
const cleanup = history.listen((_to, _from, info) => {
if (info.type !== 'pop') {
return;
}
navigation = 'pop';
});
if (import.meta.hot) {
onScopeDispose(cleanup);
}
}
{
const cleanup = router.afterEach((_to, _from, failure) => {
if (navigation === undefined || isNavigationFailure(failure)) {
return;
}
let entries = stack.value;
const length = entries.length;
const index = history.state.position - 1;
switch (navigation) {
case 'pop': {
if (index >= entries.length) {
const delta = index - length;
const extras = arr(delta + 1, (i) => (i === delta ? createEntry() : null));
entries = entries.concat(extras);
} else if (entries[index] === null) {
entries = entries.with(index, createEntry());
}
break;
}
case 'push': {
entries = entries.toSpliced(index, entries.length, createEntry());
break;
}
case 'replace': {
entries = entries.with(index, createEntry());
break;
}
}
stack.value = entries;
});
if (import.meta.hot) {
onScopeDispose(cleanup);
}
}
return { currentEntry };
});
const createEntry = (): HistoryEntry => {
return {
id: nanoid(),
};
};
const arr = <T>(length: number, map: (index: number) => T): T[] => {
return Array.from({ length }, (_, idx) => map(idx));
};
declare module 'vue-router' {
export interface HistoryState {
position: number;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment