Skip to content

Conversation

@leesb971204
Copy link
Contributor

@leesb971204 leesb971204 commented Sep 26, 2025

fixes #5209

Summary by CodeRabbit

  • Bug Fixes

    • Internal links now produce correct, router-normalized URLs and no longer yield empty-path edge cases.
    • Links recompute when routing history changes, preventing stale hrefs.
  • Tests

    • Added tests verifying correct href and target="_blank" behavior under hash-history routing for both React and Solid routers.
  • New Features

    • Exposed hash-history support for consumers.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

Walkthrough

Internal link normalization in React and Solid Link components now routes stripped-origin paths through router.history.createHref(...). React’s Link also adds router.history to the memo dependency array. Tests for hash-history behavior with target="_blank" were added for both frameworks.

Changes

Cohort / File(s) Summary
React Link normalization & deps
packages/react-router/src/link.tsx
For internal hrefs, pass the origin-stripped path into router.history.createHref(...) before defaulting; add router.history to the memo dependencies so href recalculates when history changes.
Solid Link normalization
packages/solid-router/src/link.tsx
For internal hrefs, wrap the origin-stripped path with router.history.createHref(...) instead of using the raw replaced string.
Hash-history link tests (React & Solid)
packages/react-router/tests/link.test.tsx, packages/solid-router/tests/link.test.tsx
Add test suites verifying hash-history (createHashHistory) renders href with hash prefix for standard and target="_blank" links; validates correct href and target attributes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant L as Link (React/Solid)
  participant R as Router
  participant H as History

  U->>L: Render <Link href="...">
  L->>R: Inspect router.origin and href
  alt internal (href startsWith router.origin)
    L->>H: createHref(strippedPath)
    H-->>L: normalizedHref (e.g., "/#/about" for hash history)
  else external
    L-->>L: use original href
  end
  L-->>U: Render <a href="normalizedHref|original" ...>
  note right of L: React Link also re-evaluates when router.history changes
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I twitch my whiskers at the hash,
I trim the origin, then craft the path.
createHref hums, the routes align,
New tabs open where hashes shine.
A rabbit’s hop — small, sure, and swift 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately describes the core change—a fix in the router for hash history links using target="_blank" that were generating incorrect URLs—and follows conventional commit style without extraneous detail.
Linked Issues Check ✅ Passed The pull request directly addresses issue #5209 by updating the internal href normalization to use router.history.createHref, ensuring that links with target="_blank" in hash history mode include the hash prefix, and it adds tests that verify this behavior, thereby fulfilling the linked issue’s requirements.
Out of Scope Changes Check ✅ Passed All modifications in this PR are focused on adjusting the internal href logic for hash history links and adding corresponding tests; there are no changes unrelated to the linked issue or additional feature work included.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Sep 26, 2025

View your CI Pipeline Execution ↗ for commit 2ef5384

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 5m 26s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 3s View ↗

☁️ Nx Cloud last updated this comment at 2025-09-29 00:32:10 UTC

@leesb971204 leesb971204 force-pushed the fix/hash-history-target-blank branch from 0de856d to 7771c00 Compare September 26, 2025 00:31
…history mode

Signed-off-by: leesb971204 <leesb971204@gmail.com>
@leesb971204 leesb971204 reopened this Sep 26, 2025
@pkg-pr-new
Copy link

pkg-pr-new bot commented Sep 26, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5242

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5242

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@5242

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5242

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@5242

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@5242

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@5242

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@5242

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5242

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5242

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5242

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@5242

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@5242

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@5242

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@5242

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@5242

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@5242

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@5242

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@5242

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@5242

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5242

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@5242

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@5242

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5242

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5242

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5242

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5242

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5242

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5242

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@5242

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5242

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5242

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5242

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5242

commit: 2ef5384

if (router.origin) {
if (href.startsWith(router.origin)) {
href = href.replace(router.origin, '') || '/'
href = router.history.createHref(href.replace(router.origin, '')) || '/'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since routes with target="_blank" don’t go through the router.navigate logic, the href needs to include #.
But the hrefOption generation logic wasn't applying router.history.createHref() transformation, which is essential for hash history to format URLs correctly (adding the # prefix).

@schiller-manuel
Copy link
Contributor

some tests would be nice

Signed-off-by: leesb971204 <leesb971204@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ea543e and 2ef5384.

📒 Files selected for processing (2)
  • packages/react-router/tests/link.test.tsx (2 hunks)
  • packages/solid-router/tests/link.test.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in strict mode with extensive type safety across the codebase

Files:

  • packages/react-router/tests/link.test.tsx
  • packages/solid-router/tests/link.test.tsx
packages/{react-router,solid-router}/**

📄 CodeRabbit inference engine (AGENTS.md)

Implement React and Solid bindings/components only in packages/react-router/ and packages/solid-router/

Files:

  • packages/react-router/tests/link.test.tsx
  • packages/solid-router/tests/link.test.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
PR: TanStack/router#0
File: AGENTS.md:0-0
Timestamp: 2025-09-23T17:36:12.598Z
Learning: Applies to packages/{react-router,solid-router}/** : Implement React and Solid bindings/components only in packages/react-router/ and packages/solid-router/
🧬 Code graph analysis (1)
packages/react-router/tests/link.test.tsx (1)
packages/react-router/src/router.ts (1)
  • createRouter (80-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (1)
packages/solid-router/tests/link.test.tsx (1)

5681-5728: Nice regression coverage for hash-history links.

The new test nails the original repro: confirming both standard and target="_blank" links emit /#/… ensures the fix keeps working in hash mode. Thanks for guarding this edge case.

Comment on lines +6342 to +6389
const hashHistory = createHashHistory()

const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => {
return (
<>
<h1>Index</h1>
<Link data-testid="posts-link" to="/posts">
Posts (same tab)
</Link>
<Link data-testid="about-blank-link" to="/about" target="_blank">
About (new tab)
</Link>
</>
)
},
})

const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
component: () => <h1>Posts</h1>,
})

const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: () => <h1>About</h1>,
})

const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute, aboutRoute]),
history: hashHistory,
})

render(<RouterProvider router={router} />)

const postsLink = await screen.findByTestId('posts-link')
expect(postsLink).toHaveAttribute('href', '/#/posts')
expect(postsLink).not.toHaveAttribute('target', '_blank')

const postsBlankLink = await screen.findByTestId('about-blank-link')
expect(postsBlankLink).toHaveAttribute('href', '/#/about')
expect(postsBlankLink).toHaveAttribute('target', '_blank')
})
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 | 🟠 Major

Destroy the hash history instance after the test.

createHashHistory() hooks a window hashchange listener. Unlike the other tests, this block never tears it down (the global afterEach only destroys the createBrowserHistory from beforeEach). Leaving the hash history alive can bleed listeners and state into subsequent tests. Please ensure we destroy it—either reassign history = hashHistory so the existing cleanup handles it, or explicitly call hashHistory.destroy() in a finally block here.

-    const hashHistory = createHashHistory()
+    const hashHistory = createHashHistory()
+    try {-    expect(postsBlankLink).toHaveAttribute('target', '_blank')
+    expect(postsBlankLink).toHaveAttribute('target', '_blank')
+    } finally {
+      hashHistory.destroy()
+    }
📝 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
const hashHistory = createHashHistory()
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => {
return (
<>
<h1>Index</h1>
<Link data-testid="posts-link" to="/posts">
Posts (same tab)
</Link>
<Link data-testid="about-blank-link" to="/about" target="_blank">
About (new tab)
</Link>
</>
)
},
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
component: () => <h1>Posts</h1>,
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: () => <h1>About</h1>,
})
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute, aboutRoute]),
history: hashHistory,
})
render(<RouterProvider router={router} />)
const postsLink = await screen.findByTestId('posts-link')
expect(postsLink).toHaveAttribute('href', '/#/posts')
expect(postsLink).not.toHaveAttribute('target', '_blank')
const postsBlankLink = await screen.findByTestId('about-blank-link')
expect(postsBlankLink).toHaveAttribute('href', '/#/about')
expect(postsBlankLink).toHaveAttribute('target', '_blank')
})
const hashHistory = createHashHistory()
try {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => {
return (
<>
<h1>Index</h1>
<Link data-testid="posts-link" to="/posts">
Posts (same tab)
</Link>
<Link data-testid="about-blank-link" to="/about" target="_blank">
About (new tab)
</Link>
</>
)
},
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
component: () => <h1>Posts</h1>,
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: () => <h1>About</h1>,
})
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute, aboutRoute]),
history: hashHistory,
})
render(<RouterProvider router={router} />)
const postsLink = await screen.findByTestId('posts-link')
expect(postsLink).toHaveAttribute('href', '/#/posts')
expect(postsLink).not.toHaveAttribute('target', '_blank')
const postsBlankLink = await screen.findByTestId('about-blank-link')
expect(postsBlankLink).toHaveAttribute('href', '/#/about')
expect(postsBlankLink).toHaveAttribute('target', '_blank')
} finally {
hashHistory.destroy()
}
})
🤖 Prompt for AI Agents
packages/react-router/tests/link.test.tsx around lines 6342 to 6389: the test
creates a hash history with createHashHistory() but never destroys it, leaking a
window hashchange listener into later tests; fix by ensuring the instance is
cleaned up—either assign the created hashHistory to the test-level history
variable so the shared afterEach cleanup will destroy it, or wrap the test logic
in try/finally and call hashHistory.destroy() in the finally block to explicitly
remove the listener.

@schiller-manuel schiller-manuel merged commit e52673b into TanStack:main Sep 29, 2025
6 checks passed
@schiller-manuel
Copy link
Contributor

thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

<Link target="_blank"> broken with hash history

2 participants