Skip to content

fix: Fix scroll restoration #3191

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

Merged
merged 32 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1741769
fix: scroll-restoration (especially on hydration mismatch)
tannerlinsley Jan 17, 2025
df16ebd
fix: more fixes
tannerlinsley Jan 18, 2025
bda0550
fix canGoBack
schiller-manuel Jan 18, 2025
b25fa71
release: v1.97.25
tannerlinsley Jan 28, 2025
588ec74
chore(root): upgrade `vitest` to `3.0.4` (#3257)
SeanCassiere Jan 28, 2025
8662b4c
chore(root): upgrade `nx` to `20.4.0` (#3259)
SeanCassiere Jan 28, 2025
feb9a84
fix(react-router): improve error handling for module loading failures…
Sheraff Jan 28, 2025
2c904b8
fix(start-server): remove debugging timeout (#3258)
schiller-manuel Jan 28, 2025
e030e68
fix(start-plugin): invoke server implementation of createIsomorphicFn…
djgrant Jan 28, 2025
34d0350
feat(react-router): add `remountDeps` (#3269)
schiller-manuel Jan 29, 2025
2519c65
fix: RELEASE_ALL
schiller-manuel Jan 29, 2025
9c98783
examples: remove dep (#3270)
schiller-manuel Jan 29, 2025
d5d65a2
release: v1.98.0
tannerlinsley Jan 29, 2025
7fca612
checkpoint
tannerlinsley Jan 29, 2025
5b404d3
fix tests
tannerlinsley Jan 29, 2025
ffd7760
tests: fix
tannerlinsley Jan 29, 2025
23e3e61
fix: back to old onRendered
tannerlinsley Jan 29, 2025
52dc370
fix: scroll-restoration (especially on hydration mismatch)
tannerlinsley Jan 17, 2025
1c5f6b0
fix: more fixes
tannerlinsley Jan 18, 2025
3a2268e
fix canGoBack
schiller-manuel Jan 18, 2025
d1b048b
checkpoint
tannerlinsley Jan 29, 2025
131908f
fix tests
tannerlinsley Jan 29, 2025
dc66e62
tests: fix
tannerlinsley Jan 29, 2025
47bca5a
fix: back to old onRendered
tannerlinsley Jan 29, 2025
e2f93af
Merge branch 'fix-scroll-restoration' of https://github.com/TanStack/…
tannerlinsley Jan 29, 2025
dee74d8
Move utils to core
tannerlinsley Jan 29, 2025
e2b97ad
fix: backwards compat ScrollRestoration
tannerlinsley Jan 29, 2025
8dff412
fix: better versioning
tannerlinsley Jan 29, 2025
bf8c086
fix: reenable hash scrolling and window reset even without scroll res…
tannerlinsley Jan 29, 2025
7c0c256
test(e2e): renable tests for router
SeanCassiere Jan 29, 2025
fdd1c58
fix: backwards compat and docs
tannerlinsley Jan 29, 2025
a40e193
Merge branch 'fix-scroll-restoration' of https://github.com/TanStack/…
tannerlinsley Jan 29, 2025
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Quer

To run example React projects with Tanstack Router, see [CONTRIBUTING.md](./CONTRIBUTING.md)

<!-- Use the force, Luke! -->
<!-- Use the force, Luke!! -->
2 changes: 1 addition & 1 deletion docs/eslint/create-route-property-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The correct property order is as follows
- `context`
- `beforeLoad`
- `loader`
- `onEnter`, `onStay`, `onLeave`, `meta`, `links`, `scripts`, `headers`
- `onEnter`, `onStay`, `onLeave`, `meta`, `links`, `scripts`, `headers`, `remountDeps`

All other properties are insensitive to the order as they do not depend on type inference.

Expand Down
32 changes: 32 additions & 0 deletions docs/framework/react/api/router/RouteOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,35 @@ type loaderDeps = (opts: { search: TFullSearchSchema }) => Record<string, any>
- Type: `(error: Error, errorInfo: ErrorInfo) => void`
- Optional - Defaults to `routerOptions.defaultOnCatch`
- A function that will be called when errors are caught when the route encounters an error.

### `remountDeps` method

- Type:

```tsx
type remountDeps = (opts: RemountDepsOptions) => any

interface RemountDepsOptions<
in out TRouteId,
in out TFullSearchSchema,
in out TAllParams,
in out TLoaderDeps,
> {
routeId: TRouteId
search: TFullSearchSchema
params: TAllParams
loaderDeps: TLoaderDeps
}
```

- Optional
- A function that will be called to determine whether a route component shall be remounted after navigation. If this function returns a different value than previously, it will remount.
- The return value needs to be JSON serializable.
- By default, a route component will not be remounted if it stays active after a navigation

Example:
If you want to configure to remount a route component upon `params` change, use:

```tsx
remountDeps: ({ params }) => params
```
19 changes: 14 additions & 5 deletions docs/framework/react/api/router/RouterEventsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,48 @@ The `RouterEvents` type contains all of the events that the router can emit. Eac
type RouterEvents = {
onBeforeNavigate: {
type: 'onBeforeNavigate'
fromLocation: ParsedLocation
fromLocation?: ParsedLocation
toLocation: ParsedLocation
pathChanged: boolean
hrefChanged: boolean
}
onBeforeLoad: {
type: 'onBeforeLoad'
fromLocation: ParsedLocation
fromLocation?: ParsedLocation
toLocation: ParsedLocation
pathChanged: boolean
hrefChanged: boolean
}
onLoad: {
type: 'onLoad'
fromLocation: ParsedLocation
fromLocation?: ParsedLocation
toLocation: ParsedLocation
pathChanged: boolean
hrefChanged: boolean
}
onResolved: {
type: 'onResolved'
fromLocation: ParsedLocation
fromLocation?: ParsedLocation
toLocation: ParsedLocation
pathChanged: boolean
hrefChanged: boolean
}
onBeforeRouteMount: {
type: 'onBeforeRouteMount'
fromLocation: ParsedLocation
fromLocation?: ParsedLocation
toLocation: ParsedLocation
pathChanged: boolean
hrefChanged: boolean
}
onInjectedHtml: {
type: 'onInjectedHtml'
promise: Promise<string>
}
onRendered: {
type: 'onRendered'
fromLocation?: ParsedLocation
toLocation: ParsedLocation
}
}
```

Expand Down
32 changes: 32 additions & 0 deletions docs/framework/react/api/router/RouterOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,35 @@ const router = createRouter({
- Defaults to `false`
- Configures whether structural sharing is enabled by default for fine-grained selectors.
- See the [Render Optimizations guide](../../guide/render-optimizations.md) for more information.

### `defaultRemountDeps` property

- Type:

```tsx
type defaultRemountDeps = (opts: RemountDepsOptions) => any

interface RemountDepsOptions<
in out TRouteId,
in out TFullSearchSchema,
in out TAllParams,
in out TLoaderDeps,
> {
routeId: TRouteId
search: TFullSearchSchema
params: TAllParams
loaderDeps: TLoaderDeps
}
```

- Optional
- A default function that will be called to determine whether a route component shall be remounted after navigation. If this function returns a different value than previously, it will remount.
- The return value needs to be JSON serializable.
- By default, a route component will not be remounted if it stays active after a navigation

Example:
If you want to configure to remount all route components upon `params` change, use:

```tsx
remountDeps: ({ params }) => params
```
2 changes: 2 additions & 0 deletions docs/framework/react/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Feature/Capability Key:
| `<Block>`/`useBlocker` | ✅ | 🔶 | ❓ |
| Deferred Primitives | ✅ | ✅ | ✅ |
| Navigation Scroll Restoration | ✅ | ✅ | ❓ |
| ElementScroll Restoration | ✅ | 🛑 | 🛑 |
| Async Scroll Restoration | ✅ | 🛑 | 🛑 |
| Router Invalidation | ✅ | ✅ | ✅ |
| Runtime Route Manipulation (Fog of War) | 🛑 | ✅ | ✅ |
| -- | -- | -- | -- |
Expand Down
75 changes: 30 additions & 45 deletions docs/framework/react/guide/scroll-restoration.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ id: scroll-restoration
title: Scroll Restoration
---

## Hash/Top-of-Page Scrolling

Out of the box, TanStack Router supports both **hash scrolling** and **top-of-page scrolling** without any additional configuration.

## Scroll Restoration

Scroll restoration is the process of restoring the scroll position of a page when the user navigates back to it. This is normally a built-in feature for standard HTML based websites, but can be difficult to replicate for SPA applications because:

- SPAs typically use the `history.pushState` API for navigation, so the browser doesn't know to restore the scroll position natively
Expand All @@ -24,19 +30,15 @@ It does this by:
That may sound like a lot, but for you, it's as simple as this:

```tsx
import { ScrollRestoration } from '@tanstack/react-router'
import { createRouter } from '@tanstack/react-router'

function Root() {
return (
<>
<ScrollRestoration />
<Outlet />
</>
)
}
const router = createRouter({
scrollRestoration: true,
})
```

Just render the `ScrollRestoration` component (or use the `useScrollRestoration` hook) at the root of your application and it will handle everything automatically!
> [!NOTE]
> The `<ScrollRestoration />` component still works, but has been deprecated.

## Custom Cache Keys

Expand All @@ -51,38 +53,26 @@ The default `getKey` is `(location) => location.state.key!`, where `key` is the
You could sync scrolling to the pathname:

```tsx
import { ScrollRestoration } from '@tanstack/react-router'
import { createRouter } from '@tanstack/react-router'

function Root() {
return (
<>
<ScrollRestoration getKey={(location) => location.pathname} />
<Outlet />
</>
)
}
const router = createRouter({
getScrollRestorationKey: (location) => location.pathname,
})
```

You can conditionally sync only some paths, then use the key for the rest:

```tsx
import { ScrollRestoration } from '@tanstack/react-router'

function Root() {
return (
<>
<ScrollRestoration
getKey={(location) => {
const paths = ['/', '/chat']
return paths.includes(location.pathname)
? location.pathname
: location.state.key!
}}
/>
<Outlet />
</>
)
}
import { createRouter } from '@tanstack/react-router'

const router = createRouter({
getScrollRestorationKey: (location) => {
const paths = ['/', '/chat']
return paths.includes(location.pathname)
? location.pathname
: location.state.key!
},
})
```

## Preventing Scroll Restoration
Expand Down Expand Up @@ -143,14 +133,9 @@ function Component() {
To control the scroll behavior when navigating between pages, you can use the `scrollBehavior` option. This allows you to make the transition between pages instant instead of a smooth scroll. The global configuration of scroll restoration behavior has the same options as those supported by the browser, which are `smooth`, `instant`, and `auto` (see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#behavior) for more information).

```tsx
import { ScrollRestoration } from '@tanstack/react-router'
import { createRouter } from '@tanstack/react-router'

function Root() {
return (
<>
<ScrollRestoration scrollBehavior="instant" />
<Outlet />
</>
)
}
const router = createRouter({
scrollBehavior: 'instant',
})
```
48 changes: 0 additions & 48 deletions e2e/react-router/basic-scroll-restoration/src/has-shown.tsx

This file was deleted.

5 changes: 2 additions & 3 deletions e2e/react-router/basic-scroll-restoration/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { useVirtualizer } from '@tanstack/react-virtual'
import HasShown from './has-shown'
import './styles.css'

const rootRoute = createRootRoute({
Expand Down Expand Up @@ -60,7 +59,7 @@ function IndexComponent() {
<h3 id="greeting" className="bg-red-600">
Welcome Home!
</h3>
<HasShown id="top-message" />
<div id="top-message" />
<div className="space-y-2">
{Array.from({ length: 50 }).map((_, i) => (
<div
Expand Down Expand Up @@ -141,7 +140,7 @@ function ByElementComponent() {
>
<div className="h-[100px] p-2 rounded-lg bg-red-600 border">
First Regular List Item
<HasShown id="first-regular-list-item" />
<div id="first-regular-list-item" />
</div>
{Array.from({ length: 50 }).map((_, i) => (
<div
Expand Down
Loading