diff --git a/docs/framework/react/api/router.md b/docs/framework/react/api/router.md
index 4078ee2536d..f45194db4be 100644
--- a/docs/framework/react/api/router.md
+++ b/docs/framework/react/api/router.md
@@ -35,6 +35,7 @@ title: Router API
- Hooks
- [`useAwaited`](./router/useAwaitedHook.md)
- [`useBlocker`](./router/useBlockerHook.md)
+ - [`useCanGoBack`](./router//useCanGoBack.md)
- [`useChildMatches`](./router/useChildMatchesHook.md)
- [`useLinkProps`](./router/useLinkPropsHook.md)
- [`useLoaderData`](./router/useLoaderDataHook.md)
diff --git a/docs/framework/react/api/router/useCanGoBack.md b/docs/framework/react/api/router/useCanGoBack.md
new file mode 100644
index 00000000000..39b84fbdf9b
--- /dev/null
+++ b/docs/framework/react/api/router/useCanGoBack.md
@@ -0,0 +1,40 @@
+---
+id: useCanGoBack
+title: useCanGoBack hook
+---
+
+The `useCanGoBack` hook returns a boolean representing if the router history can safely go back without exiting the application.
+
+> ⚠️ The following new `useCanGoBack` API is currently _experimental_.
+
+## useCanGoBack returns
+
+- If the router history is not at index `0`, `true`.
+- If the router history is at index `0`, `false`.
+
+## Limitations
+
+The router history index is reset after a navigation with [`reloadDocument`](./NavigateOptionsType.md#reloaddocument) set as `true`. This causes the router history to consider the new location as the initial one and will cause `useCanGoBack` to return `false`.
+
+## Examples
+
+### Showing a back button
+
+```tsx
+import { useRouter, useCanGoBack } from '@tanstack/react-router'
+
+function Component() {
+ const router = useRouter()
+ const canGoBack = useCanGoBack()
+
+ return (
+
+ {canGoBack ? (
+ router.history.back()}>Go back
+ ) : null}
+
+ {/* ... */}
+
+ )
+}
+```
diff --git a/e2e/react-router/basic-file-based/src/routes/__root.tsx b/e2e/react-router/basic-file-based/src/routes/__root.tsx
index 3e1e2220a27..ee9d9d2ef05 100644
--- a/e2e/react-router/basic-file-based/src/routes/__root.tsx
+++ b/e2e/react-router/basic-file-based/src/routes/__root.tsx
@@ -1,5 +1,10 @@
-import * as React from 'react'
-import { Link, Outlet, createRootRoute } from '@tanstack/react-router'
+import {
+ Link,
+ Outlet,
+ createRootRoute,
+ useCanGoBack,
+ useRouter,
+} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
export const Route = createRootRoute({
@@ -15,9 +20,20 @@ export const Route = createRootRoute({
})
function RootComponent() {
+ const router = useRouter()
+ const canGoBack = useCanGoBack()
+
return (
<>
-
+
+
router.history.back()}
+ className={!canGoBack ? 'line-through' : undefined}
+ >
+ Back
+ {' '}
-
{post.title}
+
+ {post.title}
+
{post.body}
)
diff --git a/e2e/react-router/basic-file-based/src/routes/posts.tsx b/e2e/react-router/basic-file-based/src/routes/posts.tsx
index 2c0cab4d749..360a165d913 100644
--- a/e2e/react-router/basic-file-based/src/routes/posts.tsx
+++ b/e2e/react-router/basic-file-based/src/routes/posts.tsx
@@ -11,7 +11,7 @@ function PostsComponent() {
const posts = Route.useLoaderData()
return (
-
+
{[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map(
(post) => {
@@ -22,7 +22,6 @@ function PostsComponent() {
params={{
postId: post.id,
}}
- reloadDocument={true}
className="block py-1 text-blue-600 hover:opacity-75"
activeProps={{ className: 'font-bold underline' }}
>
diff --git a/e2e/react-router/basic-file-based/tests/app.spec.ts b/e2e/react-router/basic-file-based/tests/app.spec.ts
index 50eea5f4c1b..e2cc9bec1b6 100644
--- a/e2e/react-router/basic-file-based/tests/app.spec.ts
+++ b/e2e/react-router/basic-file-based/tests/app.spec.ts
@@ -107,6 +107,80 @@ test('legacy Proceeding through blocked navigation works', async ({ page }) => {
await expect(page.getByRole('heading')).toContainText('Editing A')
})
+test('useCanGoBack correctly disables back button', async ({ page }) => {
+ const getBackButtonDisabled = async () => {
+ const backButton = page.getByTestId('back-button')
+ const isDisabled = (await backButton.getAttribute('disabled')) !== null
+ return isDisabled
+ }
+
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await expect(page.getByTestId('posts-links')).toBeInViewport()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByTestId('post-title')).toBeInViewport()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goForward()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.goBack()
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(true)
+})
+
+test('useCanGoBack correctly disables back button, using router.history and window.history', async ({
+ page,
+}) => {
+ const getBackButtonDisabled = async () => {
+ const backButton = page.getByTestId('back-button')
+ const isDisabled = (await backButton.getAttribute('disabled')) !== null
+ return isDisabled
+ }
+
+ await page.getByRole('link', { name: 'Posts' }).click()
+ await expect(page.getByTestId('posts-links')).toBeInViewport()
+ await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+ await expect(page.getByTestId('post-title')).toBeInViewport()
+ await page.getByTestId('back-button').click()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.getByTestId('back-button').click()
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.evaluate('window.history.forward()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.forward()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.back()')
+ expect(await getBackButtonDisabled()).toBe(false)
+
+ await page.evaluate('window.history.back()')
+ expect(await getBackButtonDisabled()).toBe(true)
+
+ await page.reload()
+ expect(await getBackButtonDisabled()).toBe(true)
+})
+
const testCases = [
{
description: 'Navigating to a route inside a route group',
diff --git a/packages/history/src/index.ts b/packages/history/src/index.ts
index 2a85d149505..c28d9588873 100644
--- a/packages/history/src/index.ts
+++ b/packages/history/src/index.ts
@@ -8,7 +8,7 @@ export interface NavigateOptions {
type SubscriberHistoryAction =
| {
- type: HistoryAction | 'ROLLBACK'
+ type: HistoryAction
}
| {
type: 'GO'
@@ -30,6 +30,7 @@ export interface RouterHistory {
go: (index: number, navigateOpts?: NavigateOptions) => void
back: (navigateOpts?: NavigateOptions) => void
forward: (navigateOpts?: NavigateOptions) => void
+ canGoBack: () => boolean
createHref: (href: string) => string
block: (blocker: NavigationBlocker) => () => void
flush: () => void
@@ -51,17 +52,12 @@ export interface ParsedPath {
export interface HistoryState {
key?: string
+ __TSR_index: number
}
type ShouldAllowNavigation = any
-export type HistoryAction =
- | 'PUSH'
- | 'POP'
- | 'REPLACE'
- | 'FORWARD'
- | 'BACK'
- | 'GO'
+export type HistoryAction = 'PUSH' | 'REPLACE' | 'FORWARD' | 'BACK' | 'GO'
export type BlockerFnArgs = {
currentLocation: HistoryLocation
@@ -93,6 +89,7 @@ type TryNavigateArgs = {
}
)
+const stateIndexKey = '__TSR_index'
const popStateEvent = 'popstate'
const beforeUnloadEvent = 'beforeunload'
@@ -107,9 +104,11 @@ export function createHistory(opts: {
createHref: (path: string) => string
flush?: () => void
destroy?: () => void
- onBlocked?: (onUpdate: () => void) => void
+ onBlocked?: () => void
getBlockers?: () => Array
setBlockers?: (blockers: Array) => void
+ // Avoid notifying on forward/back/go, used for browser history as we already get notified by the popstate event
+ notifyOnIndexChange?: boolean
}): RouterHistory {
let location = opts.getLocation()
const subscribers = new Set<(opts: SubscriberArgs) => void>()
@@ -119,11 +118,9 @@ export function createHistory(opts: {
subscribers.forEach((subscriber) => subscriber({ location, action }))
}
- const _notifyRollback = () => {
- location = opts.getLocation()
- subscribers.forEach((subscriber) =>
- subscriber({ location, action: { type: 'ROLLBACK' } }),
- )
+ const handleIndexChange = (action: SubscriberHistoryAction) => {
+ if (opts.notifyOnIndexChange ?? true) notify(action)
+ else location = opts.getLocation()
}
const tryNavigation = async ({
@@ -149,7 +146,7 @@ export function createHistory(opts: {
action: actionInfo.type,
})
if (isBlocked) {
- opts.onBlocked?.(_notifyRollback)
+ opts.onBlocked?.()
return
}
}
@@ -174,7 +171,8 @@ export function createHistory(opts: {
}
},
push: (path, state, navigateOpts) => {
- state = assignKey(state)
+ const currentIndex = location.state[stateIndexKey]
+ state = assignKeyAndIndex(currentIndex + 1, state)
tryNavigation({
task: () => {
opts.pushState(path, state)
@@ -187,7 +185,8 @@ export function createHistory(opts: {
})
},
replace: (path, state, navigateOpts) => {
- state = assignKey(state)
+ const currentIndex = location.state[stateIndexKey]
+ state = assignKeyAndIndex(currentIndex, state)
tryNavigation({
task: () => {
opts.replaceState(path, state)
@@ -203,7 +202,7 @@ export function createHistory(opts: {
tryNavigation({
task: () => {
opts.go(index)
- notify({ type: 'GO', index })
+ handleIndexChange({ type: 'GO', index })
},
navigateOpts,
type: 'GO',
@@ -213,7 +212,7 @@ export function createHistory(opts: {
tryNavigation({
task: () => {
opts.back(navigateOpts?.ignoreBlocker ?? false)
- notify({ type: 'BACK' })
+ handleIndexChange({ type: 'BACK' })
},
navigateOpts,
type: 'BACK',
@@ -223,12 +222,13 @@ export function createHistory(opts: {
tryNavigation({
task: () => {
opts.forward(navigateOpts?.ignoreBlocker ?? false)
- notify({ type: 'FORWARD' })
+ handleIndexChange({ type: 'FORWARD' })
},
navigateOpts,
type: 'FORWARD',
})
},
+ canGoBack: () => location.state[stateIndexKey] !== 0,
createHref: (str) => opts.createHref(str),
block: (blocker) => {
if (!opts.setBlockers) return () => {}
@@ -246,13 +246,14 @@ export function createHistory(opts: {
}
}
-function assignKey(state: HistoryState | undefined) {
+function assignKeyAndIndex(index: number, state: HistoryState | undefined) {
if (!state) {
state = {} as HistoryState
}
return {
...state,
key: createRandomKey(),
+ [stateIndexKey]: index,
}
}
@@ -301,6 +302,7 @@ export function createBrowserHistory(opts?: {
let currentLocation = parseLocation()
let rollbackLocation: HistoryLocation | undefined
+ let nextPopIsGo = false
let ignoreNextPop = false
let skipBlockerNextPop = false
let ignoreNextBeforeUnload = false
@@ -375,9 +377,10 @@ export function createBrowserHistory(opts?: {
}
}
- const onPushPop = () => {
+ // NOTE: this function can probably be removed
+ const onPushPop = (type: 'PUSH' | 'REPLACE') => {
currentLocation = parseLocation()
- history.notify({ type: 'POP' })
+ history.notify({ type })
}
const onPushPopEvent = async () => {
@@ -386,22 +389,39 @@ export function createBrowserHistory(opts?: {
return
}
+ const nextLocation = parseLocation()
+ const delta =
+ nextLocation.state[stateIndexKey] - currentLocation.state[stateIndexKey]
+ const isForward = delta === 1
+ const isBack = delta === -1
+ const isGo = (!isForward && !isBack) || nextPopIsGo
+ nextPopIsGo = false
+
+ const action = isGo ? 'GO' : isBack ? 'BACK' : 'FORWARD'
+ const notify: SubscriberHistoryAction = isGo
+ ? {
+ type: 'GO',
+ index: delta,
+ }
+ : {
+ type: isBack ? 'BACK' : 'FORWARD',
+ }
+
if (skipBlockerNextPop) {
skipBlockerNextPop = false
} else {
const blockers = _getBlockers()
if (typeof document !== 'undefined' && blockers.length) {
for (const blocker of blockers) {
- const nextLocation = parseLocation()
const isBlocked = await blocker.blockerFn({
currentLocation,
nextLocation,
- action: 'POP',
+ action,
})
if (isBlocked) {
ignoreNextPop = true
win.history.go(1)
- history.notify({ type: 'POP' })
+ history.notify(notify)
return
}
}
@@ -409,7 +429,7 @@ export function createBrowserHistory(opts?: {
}
currentLocation = parseLocation()
- history.notify({ type: 'POP' })
+ history.notify(notify)
}
const onBeforeUnload = (e: BeforeUnloadEvent) => {
@@ -462,7 +482,10 @@ export function createBrowserHistory(opts?: {
ignoreNextBeforeUnload = true
win.history.forward()
},
- go: (n) => win.history.go(n),
+ go: (n) => {
+ nextPopIsGo = true
+ win.history.go(n)
+ },
createHref: (href) => createHref(href),
flush,
destroy: () => {
@@ -473,17 +496,16 @@ export function createBrowserHistory(opts?: {
})
win.removeEventListener(popStateEvent, onPushPopEvent)
},
- onBlocked: (onUpdate) => {
+ onBlocked: () => {
// If a navigation is blocked, we need to rollback the location
// that we optimistically updated in memory.
if (rollbackLocation && currentLocation !== rollbackLocation) {
currentLocation = rollbackLocation
- // Notify subscribers
- onUpdate()
}
},
getBlockers: _getBlockers,
setBlockers: _setBlockers,
+ notifyOnIndexChange: false,
})
win.addEventListener(beforeUnloadEvent, onBeforeUnload, { capture: true })
@@ -491,13 +513,13 @@ export function createBrowserHistory(opts?: {
win.history.pushState = function (...args: Array) {
const res = originalPushState.apply(win.history, args as any)
- if (!history._ignoreSubscribers) onPushPop()
+ if (!history._ignoreSubscribers) onPushPop('PUSH')
return res
}
win.history.replaceState = function (...args: Array) {
const res = originalReplaceState.apply(win.history, args as any)
- if (!history._ignoreSubscribers) onPushPop()
+ if (!history._ignoreSubscribers) onPushPop('REPLACE')
return res
}
@@ -528,8 +550,12 @@ export function createMemoryHistory(
},
): RouterHistory {
const entries = opts.initialEntries
- let index = opts.initialIndex ?? entries.length - 1
- const states = entries.map(() => ({}) as HistoryState)
+ let index = opts.initialIndex
+ ? Math.min(Math.max(opts.initialIndex, 0), entries.length - 1)
+ : entries.length - 1
+ const states = entries.map((_entry, index) =>
+ assignKeyAndIndex(index, undefined),
+ )
const getLocation = () => parseHref(entries[index]!, states[index])
@@ -587,7 +613,7 @@ export function parseHref(
searchIndex > -1
? href.slice(searchIndex, hashIndex === -1 ? undefined : hashIndex)
: '',
- state: state || {},
+ state: state || { [stateIndexKey]: 0 },
}
}
diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx
index 3a4ea4f4e4b..3b5f9f1e132 100644
--- a/packages/react-router/src/index.tsx
+++ b/packages/react-router/src/index.tsx
@@ -317,6 +317,8 @@ export { useRouterState } from './useRouterState'
export { useLocation } from './useLocation'
+export { useCanGoBack } from './useCanGoBack'
+
export {
escapeJSON, // SSR
useLayoutEffect, // SSR
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index f7b2cc13f3f..d154d5fb3a3 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -1893,7 +1893,10 @@ export class Router<
...rest
}: BuildNextOptions & CommitLocationOptions = {}) => {
if (href) {
- const parsed = parseHref(href, {})
+ const currentIndex = this.history.location.state.__TSR_index
+ const parsed = parseHref(href, {
+ __TSR_index: replace ? currentIndex : currentIndex + 1,
+ })
rest.to = parsed.pathname
rest.search = this.options.parseSearch(parsed.search)
// remove the leading `#` from the hash
diff --git a/packages/react-router/src/useCanGoBack.ts b/packages/react-router/src/useCanGoBack.ts
new file mode 100644
index 00000000000..9476a9d51f6
--- /dev/null
+++ b/packages/react-router/src/useCanGoBack.ts
@@ -0,0 +1,5 @@
+import { useRouterState } from './useRouterState'
+
+export function useCanGoBack() {
+ return useRouterState({ select: (s) => s.location.state.__TSR_index !== 0 })
+}
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index afcd2732146..8f6ffcb67af 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -16,6 +16,7 @@ import {
createRootRoute,
createRoute,
createRouter,
+ useNavigate,
} from '../src'
import type { AnyRoute, AnyRouter, RouterOptions } from '../src'
@@ -1091,3 +1092,161 @@ describe('route id uniqueness', () => {
})
})
})
+
+const createHistoryRouter = () => {
+ const rootRoute = createRootRoute()
+
+ const IndexComponent = () => {
+ const navigate = useNavigate()
+
+ return (
+ <>
+ Index
+ navigate({ to: '/' })}>Index
+ navigate({ to: '/posts' })}>Posts
+ navigate({ to: '/posts', replace: true })}>
+ Replace
+
+ >
+ )
+ }
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: IndexComponent,
+ })
+
+ const postsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/posts',
+ component: () => {
+ const navigate = useNavigate()
+
+ return (
+ <>
+ Posts
+ navigate({ to: '/' })}>Index
+ >
+ )
+ },
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ })
+
+ return { router }
+}
+
+describe('history: History gives correct notifcations and state', () => {
+ it('should work with push and back', async () => {
+ const { router: router } = createHistoryRouter()
+
+ type Router = typeof router
+
+ const results: Array<
+ Parameters[0]>[0]['action']
+ > = []
+
+ render( )
+
+ const unsub = router.history.subscribe(({ action }) => {
+ results.push(action)
+ })
+
+ const postsButton = await screen.findByRole('button', { name: 'Posts' })
+
+ fireEvent.click(postsButton)
+
+ expect(
+ await screen.findByRole('heading', { name: 'Posts' }),
+ ).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/posts')
+
+ act(() => router.history.back())
+
+ expect(
+ await screen.findByRole('heading', { name: 'Index' }),
+ ).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/')
+
+ expect(results).toEqual([{ type: 'PUSH' }, { type: 'BACK' }])
+
+ unsub()
+ })
+
+ it('should work more complex scenario', async () => {
+ const { router: router } = createHistoryRouter()
+
+ type Router = typeof router
+
+ const results: Array<
+ Parameters[0]>[0]['action']
+ > = []
+
+ render( )
+
+ const unsub = router.history.subscribe(({ action }) => {
+ results.push(action)
+ })
+
+ const replaceButton = await screen.findByRole('button', { name: 'Replace' })
+
+ fireEvent.click(replaceButton)
+
+ expect(
+ await screen.findByRole('heading', { name: 'Posts' }),
+ ).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/posts')
+
+ const indexButton = await screen.findByRole('button', { name: 'Index' })
+
+ fireEvent.click(indexButton)
+
+ expect(
+ await screen.findByRole('heading', { name: 'Index' }),
+ ).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/')
+
+ const postsButton = await screen.findByRole('button', { name: 'Posts' })
+
+ fireEvent.click(postsButton)
+
+ expect(
+ await screen.findByRole('heading', { name: 'Posts' }),
+ ).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/posts')
+
+ act(() => router.history.back())
+
+ expect(
+ await screen.findByRole('heading', { name: 'Index' }),
+ ).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/')
+
+ act(() => router.history.go(1))
+
+ expect(
+ await screen.findByRole('heading', { name: 'Posts' }),
+ ).toBeInTheDocument()
+
+ expect(window.location.pathname).toBe('/posts')
+
+ expect(results).toEqual([
+ { type: 'REPLACE' },
+ { type: 'PUSH' },
+ { type: 'PUSH' },
+ { type: 'BACK' },
+ { type: 'GO', index: 1 },
+ ])
+
+ unsub()
+ })
+})
diff --git a/packages/react-router/tests/useBlocker.test-d.tsx b/packages/react-router/tests/useBlocker.test-d.tsx
index ed3149fe997..644a8036b01 100644
--- a/packages/react-router/tests/useBlocker.test-d.tsx
+++ b/packages/react-router/tests/useBlocker.test-d.tsx
@@ -102,7 +102,7 @@ test('shouldBlockFn has corrent action', () => {
.toHaveProperty('shouldBlockFn')
.parameter(0)
.toHaveProperty('action')
- .toEqualTypeOf<'PUSH' | 'POP' | 'REPLACE' | 'FORWARD' | 'BACK' | 'GO'>()
+ .toEqualTypeOf<'PUSH' | 'REPLACE' | 'FORWARD' | 'BACK' | 'GO'>()
expectTypeOf(useBlocker)
.parameter(0)
diff --git a/packages/react-router/tests/useCanGoBack.test.tsx b/packages/react-router/tests/useCanGoBack.test.tsx
new file mode 100644
index 00000000000..122e647d839
--- /dev/null
+++ b/packages/react-router/tests/useCanGoBack.test.tsx
@@ -0,0 +1,92 @@
+import { beforeEach, describe, expect, test } from 'vitest'
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import {
+ Link,
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ useCanGoBack,
+ useLocation,
+ useRouter,
+} from '../src'
+
+beforeEach(() => {
+ cleanup()
+})
+
+describe('useCanGoBack', () => {
+ function setup({
+ initialEntries = ['/'],
+ }: {
+ initialEntries?: Array
+ } = {}) {
+ function RootComponent() {
+ const router = useRouter()
+ const location = useLocation()
+ const canGoBack = useCanGoBack()
+
+ expect(canGoBack).toBe(location.pathname === '/' ? false : true)
+
+ return (
+ <>
+ router.history.back()}>Back
+ Home
+ About
+
+ >
+ )
+ }
+
+ const rootRoute = createRootRoute({
+ component: RootComponent,
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => IndexTitle ,
+ })
+ const aboutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/about',
+ component: () => AboutTitle ,
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, aboutRoute]),
+ history: createMemoryHistory({ initialEntries }),
+ })
+
+ return render( )
+ }
+
+ test('when no location behind', async () => {
+ setup()
+
+ const indexTitle = await screen.findByText('IndexTitle')
+ expect(indexTitle).toBeInTheDocument()
+
+ const aboutLink = await screen.findByText('About')
+ fireEvent.click(aboutLink)
+
+ const aboutTitle = await screen.findByText('AboutTitle')
+ expect(aboutTitle).toBeInTheDocument()
+ })
+
+ test('when location behind', async () => {
+ setup({
+ initialEntries: ['/', '/about'],
+ })
+
+ const aboutTitle = await screen.findByText('AboutTitle')
+ expect(aboutTitle).toBeInTheDocument()
+
+ const backButton = await screen.findByText('Back')
+ fireEvent.click(backButton)
+
+ const indexTitle = await screen.findByText('IndexTitle')
+ expect(indexTitle).toBeInTheDocument()
+ })
+})