Repository navigation
useLocation re-renders before navigating #3110
Description
🚨 To anyone stumbling here: this is not a bug, it's an issue with the documentation. All of us migrating from react-router-dom are used to using useLocation for everything, but with tanstack/router, there are different hooks for different purposes: https://tanstack.com/router/latest/docs/framework/react/api/router
👇👇👇👇👇 THIS IS THE SOLUTION TO YOUR PROBLEM 👇👇👇👇👇
Important
TL;DR:
useLocation is about the URL, you most likely do not care about the URL. What you care about is probably:
- the routes =>
useMatch,useMatches,useChildMatches,useParentMatches - the params =>
useParams,useSearch
👆👆👆👆👆 THIS IS THE SOLUTION TO YOUR PROBLEM 👆👆👆👆👆
Which project does this relate to?
Router
Describe the bug
Assuming a simple route like this:
export const Route = createFileRoute('/about')({
component: AboutComponent,
});
function AboutComponent() {
const {pathname} = useLocation()
console.log(pathname)
return (
<div className="p-2">
<h3>About</h3>
</div>
);
}My naive assumption would be that the only value pathname can ever have here is "/about", but in reality, this will also re-render just before navigating away, with pathname having the value of the next page.
Alternatively, we can also use the following:
const pathname = useRouterState({ select: (s) => s.resolvedLocation.pathname });But in that case, pathname will initially have the value of the previous page before immediately re-rendering with the current page.
One way to access the pathname that seems stable is this:
const pathname = useMatch({ strict: false, select: (s) => s.pathname });Your Example Website or App
https://stackblitz.com/edit/tanstack-router-fpv611d5?file=src%2Froutes%2F_yo%2Fabout.tsx
Steps to Reproduce the Bug or Issue
- Go to '/about' in the demo above
- open the console to see what gets logged
- Go to any other page
This will show that the console.log(pathname) (similar to the one in the code sample above) logs the next route even though it's only in the route component of /about.
The about.tsx file contains other commented-out methods of "logging the pathname" that demonstrate the various render behaviors.
Expected behavior
I'm not entirely sure what is "expected behavior" here. But my naive intuition would be that for all of these methods, since they're used inside the "/about" route, they should only ever render with the "/about" pathname.
Screenshots or Videos
Screen.Recording.2025-01-04.at.16.43.05.mov
Platform
- OS: macOS
- Browser: Chrome
- Version: 1.95.0
Additional context
This might be the same behavior that causes #2318 because their demo uses
const location = useLocation();
<ErrorBoundary
resetKeys={[location.pathname]}
Activity
hobi9 commentedon Jan 19, 2025
I’m on version 1.97.3 and noticed an issue with useMatch. When using the following code:
const pathname = useMatch({ strict: false, select: (s) => s.pathname });It initially returns '/' but does not update when the route changes. As a temporary workaround, I switched to this approach:
const { pathname } = useMatches().at(-1)!;This resolves the issue for now. Is this the intended behavior, or is this a bug in the useMatch implementation?
Sheraff commentedon Jan 19, 2025
This seems like a different thing. Is your
useMatchon a layout by any chance? If so, I think that's intended.eylamk commentedon Jan 24, 2025
I hope someone is going to take care of it... this is a tough one causing lots of redundant re-renders.
misium commentedon Feb 6, 2025
This bug got me good today. Looks like it overlaps with #1691.
Reproducing
(We can probably ignore the
beforeLoadoption in the Route, but leaving it in comment as I didn't test it exhaustively.)Once
checkLogin.isErroris true,<Navigate>is rendered, changing thelocationobject, all as expected. But as @Sheraff pointed out, our current route's<AuthWrapper>unexpectedly re-renders despitelocationchanging, which returns<Navigate>again, changing thelocationobject again, etc. in an infinite loop.Expected Behavior
Like OP, I naively expected
useLocation()to match the current rendered route!Current Fix
Digging in, it looks like
<Navigate>causeslocation.state.keyto change, even iflocation.hrefetc. isn't changing. Oncelocationchanges, the component is re-rendered, and we're in a loop. So to break the loop, I just forcedlocationto return only the stable values I needed, which stops the re-rendering, in my case:Returning
<Navigate>multiple times will NOT change pathname and href, so the component no longer re-renders and the loop is broken.xsjcTony commentedon Feb 8, 2025
This is indeed so true.
Based on the conversation, and ours in the Discord channel, the key point is that
Is this really the intended behaviour that the previous route's component doesn't unmount until pendingComponent is shown.Honestly, myself doesn't see any benefit from this default behaviour at all, where it only adds anti-intuitive behaviour of the
useLocation()hook.If a navigation is happening, the result is guaranteed to be either the error component or the new route's component, unless I'm missing something. In each case, the old route's component won't be needed at all.
🤔 So why don't we just unmount the previous route's component before the async navigation starts? At least I think we should have an opt-in way or make it the default behaviour when
pendingMsis0?I don't want to ping any maintainers (and I don't know who exactly to ping) here straight away as they are already pretty busy, but hopefully someone from the official team can give an explanation about the intention behind the scene and what's the recommended way...🙏
And regarding documentation, since a lot of guys are migrating from
react-router-dom, I think we should at leastuseLocation()hook, as it's anti-intuitive that it re-renders in the previous route's component when navigating.tannerlinsley commentedon Feb 8, 2025
This is valuable feedback. I’ll discuss with the team and see what we can come up with to alleviate it. At the very least I would immediately accept a PR to improve the docs.
eylamk commentedon Mar 24, 2025
Is there any update on this issue? It's a significant problem—every time a navigation is performed, the component for the 'from' route is rendered using the 'to' URL. If the 'from' route’s component includes a path parameter, it will be rendered without it, which causes an error.
@tannerlinsley, please prioritize it.
GarrisonBates commentedon Jul 29, 2025
+1 for this issue, I just ran into some really strange behavior when migrating from React Router which led to infinite redirect loop, caused by this. Was very unexpected.
AnderssonPeter commentedon Sep 2, 2025
I think I have found something that works more like
useLocationin react router.ILliDan04 commentedon Nov 9, 2025
Recently encountered related bug, lead to fatal behaviour in our case. We have 2 pages A and B. When B mounts, webcam access is requested. Once the user finishes its session on page B,
navigateis performed back to page A. But for whatever reason, page B is re-mounted again, and camera permission is asked again. Only then redirect to page A is performed. So we are left with page A and camera permission modal on it.When adding
const { pathname } = useLocation()to page B,console.log(pathname)outputs page A after callingnavigate.What's also worth noticing, that page B is not just rerendered, but fully remounted again, so that all states/refs/effects are re-initialized again.
Current workaround is to add this to root of page B:
Looks like unintended behaviour. Please correct me if I am wrong.
ryan-tote commentedon Feb 24, 2026
We recently did a migration from react router to tanstack router and this was a pain point for us. It seems like
useLocationshould return theresolvedLocationby default since that is what users would expect 99% of the time. Would be nice ifuseLocationtook a prop ofthat defaults to true and then if someone does want the current behaviour they can always change it to false.
My current custom hook.
AnderssonPeter commentedon Feb 25, 2026
We changed our implementation to the following:
The one i provided before had some issue but i can't remember what.
Joseph7451797 commentedon Jul 14, 2026
We hit this in production as a hard crash (React error #185, "Maximum update depth exceeded"), so adding a data point plus a minimal reproduction, since this issue seems to be the canonical home for the "old component re-renders with the NEW location before unmounting" behavior (#3340 was closed into here).
Versions:
@tanstack/react-router1.170.16 /@tanstack/router-core1.171.13 (theNavigateimplementation on currentmainis identical as of today).What happens
A render-time redirect guard (the classic
if (!user) return <Navigate to="/" search={{ redirect }} replace />pattern) can self-loop into React #185. Four ingredients, each individually deliberate:<Navigate>fires from a layout effect guarded by a reference comparison (previousPropsRef.current !== props, added in fix(react-router): only navigate once in StrictMode #3465 to dedupe StrictMode double-invocations). JSX produces a fresh props object every render, so any re-render/remount of the parent firesnavigate()again.useLayoutEffect(anti-flicker), so re-fires are synchronous — React counts them against the nested-update limit.commitLocationhandles a navigation to the current URL withif (isSameUrl && isSameState()) this.load()(a feature, per Router does not triggerloaderorbeforeLoadwhen navigating to same url #1324), andload()synchronously updates router state.?redirect=param forward) now renders a<Navigate>whose target equals the URL the router is already at.Cycle: layout effect →
navigate()to the current URL →load()→ state update → re-render/remount of the still-mounted guard → fresh props object → layout effect fires again → … → React throws #185, surfacing viaonunhandledrejection.Production stack trace excerpt (minified React 19, abbreviated — the loop frames):
Minimal reproduction (vitest + jsdom, ~50 lines)
Visiting
/dashboardwhile logged out throws #185 ~50 times as unhandled rejections; the same happens in a real browser given enough latency on the destination chunk.Workaround we shipped
A drop-in
<Navigate>replacement that skips the call when the built target already equalsrouter.latestLocation— the exact comparisoncommitLocation'sisSameUrluses — so a committed redirect can never re-trigger itself:Suggestion
Would you accept a PR making the declarative
<Navigate>component skip same-URL navigations (or compare its props by value rather than reference)? The same-URL →load()behavior makes sense for the imperativenavigate()(that's #1324's use case: an explicit call should re-run loaders), but for the declarative component it means "still rendered + already there" escalates into a synchronous self-loop and a crash. Happy to contribute the fix and port the reproduction above into the test suite.Maher-Reven commentedon Sep 24, 2026
There are two answers circulating in this thread that contradict each other — the pinned one at the top recommends
useMatch({ strict: false }), and @hobi9 reported that it returns/and never updates. Both are right, and which one you get depends on where you call it. Here are the measurements.Setup
@tanstack/react-router1.170.39, React 19.3.0, two routes, a probe component that records every accessor on every render. Navigating/about → /otherby clicking a<Link>, with the mount renders discarded so only the navigation shows.The outgoing component still renders with the new location
This part reproduces exactly as reported. The
/aboutcomponent renders once more before unmounting:/aboutrenderuseLocation().pathname/otheruseRouterState({ select: s => s.location.pathname })/otheruseRouterState({ select: s => s.resolvedLocation.pathname })/aboutuseMatch({ strict: false, select: s => s.pathname })/aboutuseMatches().at(-1).pathname/aboutresolvedLocationis not a clean substituteIt is correct on the way out but stale on the way in. The first render of the newly mounted
/othercomponent still reports/about, and only corrects on a later render:So swapping
useLocationforresolvedLocationmoves the wrong value from unmount to mount rather than removing it. That matches the note in the original post, and it is worth knowing before adopting it as a fix.Why
useMatch({ strict: false })disagrees with itselfIt resolves to the closest match in the component tree, so the answer depends on where the hook lives. Same run, same navigation, one probe inside the route component and one in the root layout:
useMatch({ strict: false })useMatches().at(-1)/aboutroute component/about✅ tracks the route/about✅<Outlet />/on every render/about→/other✅In the root layout the closest match is the root route, whose pathname is
/, so it is constant by construction — exactly what @hobi9 saw. Not a regression, and not a bug inuseMatch; it is the documented "closest match" semantics meeting a component that sits above every route.Practical summary for anyone arriving here:
useMatch({ strict: false, select: s => s.pathname }).useMatches().at(-1), which was @hobi9's own workaround and is the one that holds in both positions.useLocationis about the URL, which during a navigation is already the destination.Docs
@tannerlinsley said he would immediately accept a PR improving the docs here. I am happy to write it — a short "reading the current route" section covering the table above, the two positions, and why
resolvedLocationis not the shortcut it looks like. Say the word and I will open it against theuseLocation/useMatchAPI pages.That would not address the underlying behaviour that @Joseph7451797 and others are hitting as production crashes — the outgoing component rendering with the new location is still the thing to fix — but it would stop people finding this issue and then picking the workaround that is wrong for their call site.
Measured with Playwright against a production build, Chrome, macOS arm64.