Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/composables/useCanGoBack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function useCanGoBack() {
const canGoBack = ref(false)

if (import.meta.client) {
const router = useRouter()
canGoBack.value = router.options.history.state.back !== null
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Vue Router 4 router.options.history.state undefined null documentation

💡 Result:

In Vue Router 4, the history implementation you pass to createRouter({ history }) implements RouterHistory, which has a state: HistoryState property (documented, but marked Alpha in the API docs). (router.vuejs.org)

Why router.options.history.state can be undefined / null

  • router.options is not the recommended public surface to read from. Even though it exists, it’s not the API the docs point you to for “history state”.
  • Browser window.history.state can be null (e.g., before anything has called pushState/replaceState, or depending on how the entry was created). Vue Router also stores its own navigation info in history.state, and the docs warn you to preserve/merge it if you ever call history.replaceState() yourself. (typeerror.org)

What the documentation does recommend using

  • If your goal is to attach/read state for navigations, use the documented state option in navigation:
    • router.push({ ..., state: { ... } }) / router.replace({ ..., state: { ... } }) (router.vuejs.org)
  • Vue Router’s direction (per core discussion) is to expose this as route.state (reactive), rather than having people poke at router.options.history.state or raw window.history.state. (github.com)

Practical guidance

  • Don’t rely on router.options.history.state for app logic.
  • Use navigation state (router.push/replace) and (where available in your version) read it from route.state; otherwise, read window.history.state but assume it may be null and that Vue Router also uses it internally. (typeerror.org)

Citations:


🏁 Script executed:

find . -name "useCanGoBack.ts" -type f

Repository: npmx-dev/npmx.dev

Length of output: 94


🏁 Script executed:

cat -n ./app/composables/useCanGoBack.ts

Repository: npmx-dev/npmx.dev

Length of output: 345


🏁 Script executed:

rg "useCanGoBack" --type ts --type tsx -A 3 -B 3

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

rg "useCanGoBack" -A 3 -B 3

Repository: npmx-dev/npmx.dev

Length of output: 1595


Use the browser History API or Vue Router's navigation state; router.options.history.state is not a documented public API and accessing .back is unsafe.

router.options.history.state is an internal, undocumented property and not recommended. The proper approach in Vue Router 4 is to either:

  1. Use window.history.state directly (assuming it may be null), or
  2. Use the state option in router.push()/router.replace() and read from route.state

Beyond the API concern, the current code accesses .back without guarding against undefined or null, which will fail if the browser history state is not initialised. If you must inspect window.history.state, use optional chaining: Boolean(window.history.state?.back).

Comment thread
danielroe marked this conversation as resolved.
Outdated

return canGoBack
}
3 changes: 2 additions & 1 deletion app/pages/about.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
const router = useRouter()
const canGoBack = useCanGoBack()

interface GitHubContributor {
login: string
Expand Down Expand Up @@ -53,7 +54,7 @@ const { data: contributors, status: contributorsStatus } = useFetch<GitHubContri
type="button"
class="inline-flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
@click="router.back()"
v-show="router.options.history.state.back !== null"
v-if="canGoBack"
>
<span class="i-carbon:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
<span class="hidden sm:inline">{{ $t('nav.back') }}</span>
Expand Down
3 changes: 2 additions & 1 deletion app/pages/compare.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ definePageMeta({
})

const router = useRouter()
const canGoBack = useCanGoBack()

// Sync packages with URL query param (stable ref - doesn't change on other query changes)
const packagesParam = useRouteQuery<string>('packages', '', { mode: 'replace' })
Expand Down Expand Up @@ -118,7 +119,7 @@ useSeoMeta({
type="button"
class="inline-flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
@click="router.back()"
v-show="router.options.history.state.back !== null"
v-if="canGoBack"
>
<span class="i-carbon:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
<span class="hidden sm:inline">{{ $t('nav.back') }}</span>
Expand Down
3 changes: 2 additions & 1 deletion app/pages/privacy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defineOgImageComponent('Default', {
})

const router = useRouter()
const canGoBack = useCanGoBack()
const buildInfo = useAppConfig().buildInfo
const { locale } = useI18n()
</script>
Expand All @@ -30,7 +31,7 @@ const { locale } = useI18n()
type="button"
class="inline-flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
@click="router.back()"
v-show="router.options.history.state.back !== null"
v-if="canGoBack"
>
<span class="i-carbon:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
<span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
Expand Down
3 changes: 2 additions & 1 deletion app/pages/settings.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
const router = useRouter()
const canGoBack = useCanGoBack()
const { settings } = useSettings()
const { locale, locales, setLocale: setNuxti18nLocale } = useI18n()
const colorMode = useColorMode()
Expand Down Expand Up @@ -52,7 +53,7 @@ const setLocale: typeof setNuxti18nLocale = locale => {
type="button"
class="inline-flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0 p-1.5 -mx-1.5"
@click="router.back()"
v-show="router.options.history.state.back !== null"
v-if="canGoBack"
>
<span class="i-carbon:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
<span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
Expand Down
Loading