Skip to content

Commit

Permalink
feat(react-router): add useCanGoBack (#3017)
Browse files Browse the repository at this point in the history
also fixes that history does not notify twice for certain actions
  • Loading branch information
tomrehnstrom authored Jan 3, 2025
1 parent c5d141b commit 366848e
Show file tree
Hide file tree
Showing 13 changed files with 463 additions and 44 deletions.
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

0 comments on commit 366848e

Please sign in to comment.