Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions docs/framework/react/api/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions docs/framework/react/api/router/useCanGoBack.md
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{canGoBack ? (
<button onClick={() => router.history.back()}>Go back</button>
) : null}

{/* ... */}
</div>
)
}
```
22 changes: 19 additions & 3 deletions e2e/react-router/basic-file-based/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -15,9 +20,20 @@ export const Route = createRootRoute({
})

function RootComponent() {
const router = useRouter()
const canGoBack = useCanGoBack()

return (
<>
<div className="p-2 flex gap-2 text-lg border-b">
<div className="flex gap-2 p-2 text-lg border-b">
<button
data-testid="back-button"
disabled={!canGoBack}
onClick={() => router.history.back()}
className={!canGoBack ? 'line-through' : undefined}
>
Back
</button>{' '}
<Link
to="/"
activeProps={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ function PostComponent() {

return (
<div className="space-y-2">
<h4 className="text-xl font-bold underline">{post.title}</h4>
<h4 className="text-xl font-bold underline" data-testid="post-title">
{post.title}
</h4>
<div className="text-sm">{post.body}</div>
</div>
)
Expand Down
3 changes: 1 addition & 2 deletions e2e/react-router/basic-file-based/src/routes/posts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function PostsComponent() {
const posts = Route.useLoaderData()

return (
<div className="p-2 flex gap-2">
<div className="p-2 flex gap-2" data-testid="posts-links">
<ul className="list-disc pl-4">
{[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map(
(post) => {
Expand All @@ -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' }}
>
Expand Down
74 changes: 74 additions & 0 deletions e2e/react-router/basic-file-based/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading
Loading