You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Scroll restoration listeners are never removed, so every created router stays in memory #8775
When scrollRestoration is on, setupScrollRestoration (packages/router-core/src/scroll-restoration.ts) runs once per router, from the RouterCore constructor. It adds three global listeners:
document.addEventListener('scroll', …, true)
addEventListener('pagehide', …)
addEventListener('pageshow', …)
It keeps no reference to them, and the router has no method to remove them. Each listener closes over the router, so the router (its stores, matches, and the elements in trackedScrollTargets) cannot be garbage collected.
With one router for the life of the page this is fine. It is a leak when an app creates routers more than once, for example:
an app that renders some views in their own router instance and creates one per view (our case: each page in a stack of sliding panels has its own router, so the page under a panel stays mounted)
micro-frontends that mount and unmount a router
tests and HMR that create a new router each time
In our app, after opening and closing 20 panels, getEventListeners(document).scroll.length goes from 1 to 21. Each pagehide listener also writes the scroll cache to sessionStorage, so N routers write it N times on unload.
There is a way to release a router that is no longer used: its global listeners are removed and it can be garbage collected.
Proposed fix
setupScrollRestoration keeps the listeners (and the router subscriptions it adds), and returns a cleanup function.
RouterCore stores that cleanup and exposes it as a public method, for example router.dispose(). It is idempotent and does not touch history.scrollRestoration if another router still uses scroll restoration.
RouterProvider does not call it automatically, because the app creates the router outside React and may render it again.
I can open a PR with this and tests. Is dispose() an acceptable name and shape, or do you prefer something else (for example destroy(), or a cleanup returned from router.mount())?
Your environment
@tanstack/react-router 1.170.41 (also present on main, see scroll-restoration.ts)
Chrome 155, macOS
Related: #8024 (bare addEventListener for pagehide), #7956 (history.scrollRestoration mode).
Activity