Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 0 additions & 9 deletions packages/router-core/src/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1709,15 +1709,6 @@ export class BaseRoute<
this._to = fullPath as TrimPathRight<TFullPath>
}

clone = (other: typeof this) => {
this._path = other._path
this._id = other._id
this._fullPath = other._fullPath
this._to = other._to
this.options.getParentRoute = other.options.getParentRoute
this.children = other.children
}

addChildren: RouteAddChildrenFn<
TRegister,
TParentRoute,
Expand Down
10 changes: 7 additions & 3 deletions packages/router-plugin/src/core/code-splitter/compilers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export function compileCodeSplitReferenceRoute(
let createRouteFn: string

let modified = false as boolean
let hmrAdded = false as boolean
babel.traverse(ast, {
Program: {
enter(programPath) {
Expand Down Expand Up @@ -182,9 +183,10 @@ export function compileCodeSplitReferenceRoute(
}
if (!splittableCreateRouteFns.includes(createRouteFn)) {
// we can't split this route but we still add HMR handling if enabled
if (opts.addHmr) {
modified = true
if (opts.addHmr && !hmrAdded) {
programPath.pushContainer('body', routeHmrStatement)
modified = true
hmrAdded = true
}
// exit traversal so this route is not split
return programPath.stop()
Expand Down Expand Up @@ -307,8 +309,10 @@ export function compileCodeSplitReferenceRoute(
)()

// add HMR handling
if (opts.addHmr) {
if (opts.addHmr && !hmrAdded) {
programPath.pushContainer('body', routeHmrStatement)
modified = true
hmrAdded = true
}
} else {
// if (splitNodeMeta.splitStrategy === 'lazyFn') {
Expand Down
35 changes: 33 additions & 2 deletions packages/router-plugin/src/core/route-hmr-statement.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
import * as template from '@babel/template'
import type { AnyRoute } from '@tanstack/router-core'

type AnyRouteWithPrivateProps = AnyRoute & {
_path: string
_id: string
_fullPath: string
_to: string
}

function handleRouteUpdate(
oldRoute: AnyRouteWithPrivateProps,
newRoute: AnyRouteWithPrivateProps,
) {
newRoute._path = oldRoute._path
newRoute._id = oldRoute._id
newRoute._fullPath = oldRoute._fullPath
newRoute._to = oldRoute._to
newRoute.children = oldRoute.children
newRoute.parentRoute = oldRoute.parentRoute

const router = window.__TSR_ROUTER__!
router.routesById[newRoute.id] = newRoute
router.routesByPath[newRoute.fullPath] = newRoute
const oldRouteIndex = router.flatRoutes.indexOf(oldRoute)
if (oldRouteIndex > -1) {
router.flatRoutes[oldRouteIndex] = newRoute
}
router.invalidate({ filter: (m) => m.routeId === oldRoute.id })
}
Comment on lines +19 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Update parent/child bindings when swapping the route instance.

We copy private fields and replace the cache maps, but the parent route’s children array still holds oldRoute, and every child keeps its parentRoute pointing to the stale instance. Active matches and tree traversal walk those references, so beforeLoad/loader calls continue to use the pre-update implementation—the bug this PR is targeting. Please swap the instance in the parent’s children array and retarget each child before calling invalidate.

   newRoute.parentRoute = oldRoute.parentRoute
+
+  const parentChildren = newRoute.parentRoute?.children as
+    | Array<AnyRouteWithPrivateProps>
+    | undefined
+  if (parentChildren) {
+    const parentIndex = parentChildren.indexOf(oldRoute as AnyRouteWithPrivateProps)
+    if (parentIndex > -1) {
+      parentChildren[parentIndex] = newRoute
+    }
+  }
+
+  const childRoutes = Array.isArray(newRoute.children)
+    ? (newRoute.children as Array<AnyRouteWithPrivateProps>)
+    : undefined
+  childRoutes?.forEach((child) => {
+    child.parentRoute = newRoute
+  })
 
   const router = window.__TSR_ROUTER__!
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
newRoute.children = oldRoute.children
newRoute.parentRoute = oldRoute.parentRoute
const router = window.__TSR_ROUTER__!
router.routesById[newRoute.id] = newRoute
router.routesByPath[newRoute.fullPath] = newRoute
const oldRouteIndex = router.flatRoutes.indexOf(oldRoute)
if (oldRouteIndex > -1) {
router.flatRoutes[oldRouteIndex] = newRoute
}
router.invalidate({ filter: (m) => m.routeId === oldRoute.id })
}
newRoute.children = oldRoute.children
newRoute.parentRoute = oldRoute.parentRoute
const parentChildren = newRoute.parentRoute?.children as
| Array<AnyRouteWithPrivateProps>
| undefined
if (parentChildren) {
const parentIndex = parentChildren.indexOf(oldRoute as AnyRouteWithPrivateProps)
if (parentIndex > -1) {
parentChildren[parentIndex] = newRoute
}
}
const childRoutes = Array.isArray(newRoute.children)
? (newRoute.children as Array<AnyRouteWithPrivateProps>)
: undefined
childRoutes?.forEach((child) => {
child.parentRoute = newRoute
})
const router = window.__TSR_ROUTER__!
router.routesById[newRoute.id] = newRoute
router.routesByPath[newRoute.fullPath] = newRoute
const oldRouteIndex = router.flatRoutes.indexOf(oldRoute)
if (oldRouteIndex > -1) {
router.flatRoutes[oldRouteIndex] = newRoute
}
router.invalidate({ filter: (m) => m.routeId === oldRoute.id })
}
🤖 Prompt for AI Agents
In packages/router-plugin/src/core/route-hmr-statement.ts around lines 19–30,
after copying newRoute.children = oldRoute.children and newRoute.parentRoute =
oldRoute.parentRoute, swap the instance in the parent’s children array (if
parentRoute exists) by finding oldRoute in parent.children and replacing it with
newRoute, then iterate newRoute.children and set each child.parentRoute =
newRoute so children point at the new instance; do these updates before calling
router.invalidate so active matches and tree traversal use the new route
instance.


export const routeHmrStatement = template.statement(
`
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
if (newModule && newModule.Route && typeof newModule.Route.clone === 'function') {
newModule.Route.clone(Route)
if (Route && newModule && newModule.Route) {
(${handleRouteUpdate.toString()})(Route, newModule.Route)
}
})
}
`,
// Disable placeholder parsing so identifiers like __TSR_ROUTER__ are treated as normal identifiers instead of template placeholders
{ placeholderPattern: false },
)()
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,26 @@ export const Route = createFileRoute('/posts')({
});
if (import.meta.hot) {
import.meta.hot.accept(newModule => {
if (newModule && newModule.Route && typeof newModule.Route.clone === 'function') {
newModule.Route.clone(Route);
if (Route && newModule && newModule.Route) {
(function handleRouteUpdate(oldRoute, newRoute) {
newRoute._path = oldRoute._path;
newRoute._id = oldRoute._id;
newRoute._fullPath = oldRoute._fullPath;
newRoute._to = oldRoute._to;
newRoute.children = oldRoute.children;
newRoute.parentRoute = oldRoute.parentRoute;
const router = window.__TSR_ROUTER__;
router.routesById[newRoute.id] = newRoute;
router.routesByPath[newRoute.fullPath] = newRoute;
const oldRouteIndex = router.flatRoutes.indexOf(oldRoute);
if (oldRouteIndex > -1) {
router.flatRoutes[oldRouteIndex] = newRoute;
}
;
router.invalidate({
filter: m => m.routeId === oldRoute.id
});
})(Route, newModule.Route);
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,26 @@ export const Route = createFileRoute('/posts')({
});
if (import.meta.hot) {
import.meta.hot.accept(newModule => {
if (newModule && newModule.Route && typeof newModule.Route.clone === 'function') {
newModule.Route.clone(Route);
if (Route && newModule && newModule.Route) {
(function handleRouteUpdate(oldRoute, newRoute) {
newRoute._path = oldRoute._path;
newRoute._id = oldRoute._id;
newRoute._fullPath = oldRoute._fullPath;
newRoute._to = oldRoute._to;
newRoute.children = oldRoute.children;
newRoute.parentRoute = oldRoute.parentRoute;
const router = window.__TSR_ROUTER__;
router.routesById[newRoute.id] = newRoute;
router.routesByPath[newRoute.fullPath] = newRoute;
const oldRouteIndex = router.flatRoutes.indexOf(oldRoute);
if (oldRouteIndex > -1) {
router.flatRoutes[oldRouteIndex] = newRoute;
}
;
router.invalidate({
filter: m => m.routeId === oldRoute.id
});
})(Route, newModule.Route);
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,26 @@ export const Route = createFileRoute('/posts')({
});
if (import.meta.hot) {
import.meta.hot.accept(newModule => {
if (newModule && newModule.Route && typeof newModule.Route.clone === 'function') {
newModule.Route.clone(Route);
if (Route && newModule && newModule.Route) {
(function handleRouteUpdate(oldRoute, newRoute) {
newRoute._path = oldRoute._path;
newRoute._id = oldRoute._id;
newRoute._fullPath = oldRoute._fullPath;
newRoute._to = oldRoute._to;
newRoute.children = oldRoute.children;
newRoute.parentRoute = oldRoute.parentRoute;
const router = window.__TSR_ROUTER__;
router.routesById[newRoute.id] = newRoute;
router.routesByPath[newRoute.fullPath] = newRoute;
const oldRouteIndex = router.flatRoutes.indexOf(oldRoute);
if (oldRouteIndex > -1) {
router.flatRoutes[oldRouteIndex] = newRoute;
}
;
router.invalidate({
filter: m => m.routeId === oldRoute.id
});
})(Route, newModule.Route);
}
});
}
Loading