Repository: TanStack/router (ts)

Issue:
Redirect in beforeload with hash adds double hash to browser URL
### Which project does this relate to?

Router

### Describe the bug

When throwing a redirect in a routes beforeload, that includes a hash. The new route will have two hashtag symbols.

ie. 
```ts
throw redirect({
  to: '/about',
  hash: 'example'
});

console.log(window.location.hash)
// ?^ ##about
```.

### Your Example Website or App

https://stackblitz.com/edit/tanstack-router-wsmbuc?file=src%2Froutes%2Findex.tsx

### Steps to Reproduce the Bug or Issue

1. Load any route that doesn't include a hash
2. Refer to the page which should show the `window.location.hash`
3. There is two hashtags

### Expected behavior

When I redirect in before load, and the beforeload has a hash, there should only be one hashtag symbol in the route.

### Screenshots or Videos

_No response_

### Platform

- OS: [e.g. macOS, Windows, Linux]
- Browser: [e.g. Chrome, Safari, Firefox]
- Version: [e.g. 91.1]


### Additional context

This seems to be fine for normal navigate function or links, it's just the redirect function in before load (and possible the loader although untested)

Pull request description:
fix(react-router): correctly handle hashes in `redirect`
fixes #2537

Patch:
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index ab1872372..87a1d48c1 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -1657,7 +1657,8 @@ export class Router<
       const parsed = parseHref(href, {})
       rest.to = parsed.pathname
       rest.search = this.options.parseSearch(parsed.search)
-      rest.hash = parsed.hash
+      // remove the leading `#` from the hash
+      rest.hash = parsed.hash.slice(1)
     }
 
     const location = this.buildLocation({


Test patch:
diff --git a/packages/history/tests/parseHref.test.ts b/packages/history/tests/parseHref.test.ts
new file mode 100644
index 000000000..93388f411
--- /dev/null
+++ b/packages/history/tests/parseHref.test.ts
@@ -0,0 +1,11 @@
+import { describe, expect, test } from 'vitest'
+import { parseHref } from '../src'
+
+describe('parseHref', () => {
+  test('works', () => {
+    const parsed = parseHref('/foo?bar=baz#qux', {})
+    expect(parsed.pathname).toEqual('/foo')
+    expect(parsed.search).toEqual('?bar=baz')
+    expect(parsed.hash).toEqual('#qux')
+  })
+})
diff --git a/packages/react-router/tests/redirect.test.tsx b/packages/react-router/tests/redirect.test.tsx
index b9b252a50..fccd856d5 100644
--- a/packages/react-router/tests/redirect.test.tsx
+++ b/packages/react-router/tests/redirect.test.tsx
@@ -122,7 +122,11 @@ describe('redirect', () => {
         path: '/about',
         loader: async () => {
           await sleep(WAIT_TIME)
-          throw redirect({ to: '/nested/foo' })
+          throw redirect({
+            to: '/nested/foo',
+            hash: 'some-hash',
+            search: { someSearch: 'hello123' },
+          })
         },
       })
       const nestedRoute = createRoute({
@@ -134,6 +138,11 @@ describe('redirect', () => {
         },
       })
       const fooRoute = createRoute({
+        validateSearch: (search) => {
+          return {
+            someSearch: search.someSearch as string,
+          }
+        },
         getParentRoute: () => nestedRoute,
         path: '/foo',
         loader: async () => {
@@ -161,7 +170,9 @@ describe('redirect', () => {
 
       expect(fooElement).toBeInTheDocument()
 
-      expect(router.state.location.href).toBe('/nested/foo')
+      expect(router.state.location.href).toBe(
+        '/nested/foo?someSearch=hello123#some-hash',
+      )
       expect(window.location.pathname).toBe('/nested/foo')
 
       expect(nestedLoaderMock).toHaveBeenCalled()