Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ensured that history does not notify twice for certain actions and has index in state #3017

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
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>
)
}
```
16 changes: 14 additions & 2 deletions e2e/react-router/basic-file-based/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
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 +21,15 @@ 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 disabled={!canGoBack} onClick={() => router.history.back()}>
Back
</button>{' '}
<Link
to="/"
activeProps={{
Expand Down
70 changes: 70 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,76 @@ 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 = await page.getByRole('button', { name: 'Back' })
const isDisabled = (await backButton.getAttribute('disabled')) !== null
return isDisabled
}

await expect(await getBackButtonDisabled()).toBe(true)

await page.getByRole('link', { name: 'Posts' }).click()
await expect(await getBackButtonDisabled()).toBe(false)

await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
await expect(await getBackButtonDisabled()).toBe(false)

await page.reload()
await expect(await getBackButtonDisabled()).toBe(false)

await page.goBack()
await expect(await getBackButtonDisabled()).toBe(false)

await page.goForward()
await expect(await getBackButtonDisabled()).toBe(false)

await page.goBack()
await expect(await getBackButtonDisabled()).toBe(false)

await page.goBack()
await expect(await getBackButtonDisabled()).toBe(true)

await page.reload()
await expect(await getBackButtonDisabled()).toBe(true)
})

test('useCanGoBack correctly disables back button, using router.history and window.history', async ({
page,
}) => {
const getBackButtonDisabled = async () => {
const backButton = await page.getByRole('button', { name: 'Back' })
const isDisabled = (await backButton.getAttribute('disabled')) !== null
return isDisabled
}

await page.getByRole('link', { name: 'Posts' }).click()
await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
await page.getByRole('button', { name: 'Back' }).click()
await expect(await getBackButtonDisabled()).toBe(false)

await page.reload()
await expect(await getBackButtonDisabled()).toBe(false)

await page.getByRole('button', { name: 'Back' }).click()
await expect(await getBackButtonDisabled()).toBe(true)

await page.evaluate('window.history.forward()')
await expect(await getBackButtonDisabled()).toBe(false)

await page.evaluate('window.history.forward()')
await expect(await getBackButtonDisabled()).toBe(false)

await page.evaluate('window.history.back()')
await expect(await getBackButtonDisabled()).toBe(false)

await page.evaluate('window.history.back()')
await expect(await getBackButtonDisabled()).toBe(true)

await page.reload()
await expect(await getBackButtonDisabled()).toBe(true)
})

const testCases = [
{
description: 'Navigating to a route inside a route group',
Expand Down
Loading
Loading