11import * as React from 'react' ;
22import { ReactNode , forwardRef , useCallback , useMemo } from 'react' ;
33import {
4- useNavigate as useTanStackNavigate ,
54 useLocation as useTanStackLocation ,
65 useRouter ,
76 useBlocker as useTanStackBlocker ,
87 Link as TanStackLink ,
9- Navigate as TanStackNavigate ,
108 Outlet as TanStackOutlet ,
119 createRouter ,
1210 createRootRoute ,
@@ -259,7 +257,6 @@ const useLocation = (): RouterLocation => {
259257} ;
260258
261259const useNavigate = ( ) : RouterNavigateFunction => {
262- const navigate = useTanStackNavigate ( ) ;
263260 const router = useRouter ( ) ;
264261 const basename = useBasename ( ) ;
265262
@@ -289,38 +286,40 @@ const useNavigate = (): RouterNavigateFunction => {
289286 // If no pathname provided, keep current pathname
290287 const currentPath = router . state . location . pathname ;
291288 const targetPath = loc . pathname ?? currentPath ;
292- const resolvedPath = resolvePath ( targetPath ) ;
289+ let resolvedPath = resolvePath ( targetPath ) ;
293290
294- // Build the full URL with search and hash
295- // We use the history API directly to avoid TanStack Router's
296- // search param serialization which can cause double-encoding
297- let url = resolvedPath ;
291+ // Append search and hash directly to the path to preserve the raw
292+ // query string format. TanStack Router's search prop uses JSON
293+ // serialization which is incompatible with standard URL query strings.
298294 if ( loc . search ) {
299- url += loc . search . startsWith ( '?' )
295+ resolvedPath += loc . search . startsWith ( '?' )
300296 ? loc . search
301297 : `?${ loc . search } ` ;
302298 }
303299 if ( loc . hash ) {
304- url += loc . hash . startsWith ( '#' ) ? loc . hash : `#${ loc . hash } ` ;
300+ resolvedPath += loc . hash . startsWith ( '#' )
301+ ? loc . hash
302+ : `#${ loc . hash } ` ;
305303 }
306304
307305 const state = loc . state || options ?. state ;
308- if ( options ?. replace ) {
309- router . history . replace ( url , state ) ;
310- } else {
311- router . history . push ( url , state ) ;
312- }
306+ router . navigate ( {
307+ to : resolvedPath ,
308+ state ,
309+ replace : options ?. replace ,
310+ } ) ;
313311 return ;
314312 }
315313
316314 // Handle string path
317- navigate ( {
318- to : resolvePath ( to as string ) ,
315+ const resolvedPath = resolvePath ( to as string ) ;
316+ router . navigate ( {
317+ to : resolvedPath ,
319318 state : options ?. state ,
320319 replace : options ?. replace ,
321- } as any ) ;
320+ } ) ;
322321 } ,
323- [ navigate , router , basename ]
322+ [ router , basename ]
324323 ) as RouterNavigateFunction ;
325324} ;
326325
@@ -361,6 +360,19 @@ const useMatch = (pattern: {
361360 return matchPath ( pattern , pathname ) ;
362361} ;
363362
363+ // Helper to convert search object to search string
364+ const serializeSearch = ( search : Record < string , unknown > ) : string => {
365+ if ( ! search || Object . keys ( search ) . length === 0 ) return '' ;
366+ const params = new URLSearchParams ( ) ;
367+ for ( const [ key , value ] of Object . entries ( search ) ) {
368+ if ( value !== undefined && value !== null ) {
369+ params . append ( key , String ( value ) ) ;
370+ }
371+ }
372+ const str = params . toString ( ) ;
373+ return str ? `?${ str } ` : '' ;
374+ } ;
375+
364376const useBlocker = (
365377 shouldBlock : RouterBlockerFunction | boolean
366378) : RouterBlocker => {
@@ -375,17 +387,24 @@ const useBlocker = (
375387 shouldBlockFn : ( { current, next, action } ) => {
376388 const currentShouldBlock = shouldBlockRef . current ;
377389 if ( typeof currentShouldBlock === 'function' ) {
390+ // TanStack Router's ShouldBlockFnLocation only provides pathname and search (as object).
391+ // It doesn't provide hash or state at this level. We serialize search to a string
392+ // and use empty values for hash/state as they're not available.
378393 return currentShouldBlock ( {
379394 currentLocation : {
380395 pathname : current . pathname ,
381- search : '' ,
396+ search : serializeSearch (
397+ current . search as Record < string , unknown >
398+ ) ,
382399 hash : '' ,
383400 state : { } ,
384401 key : '' ,
385402 } ,
386403 nextLocation : {
387404 pathname : next . pathname ,
388- search : '' ,
405+ search : serializeSearch (
406+ next . search as Record < string , unknown >
407+ ) ,
389408 hash : '' ,
390409 state : { } ,
391410 key : '' ,
@@ -405,7 +424,11 @@ const useBlocker = (
405424 reset : blocker . reset ! ,
406425 location : {
407426 pathname : blocker . next ?. pathname ?? '' ,
408- search : '' ,
427+ search : blocker . next
428+ ? serializeSearch (
429+ blocker . next . search as Record < string , unknown >
430+ )
431+ : '' ,
409432 hash : '' ,
410433 state : { } ,
411434 key : '' ,
@@ -438,21 +461,28 @@ const Link = forwardRef<HTMLAnchorElement, RouterLinkProps>(
438461 // Handle object `to` (e.g., { pathname: '/path', search: '?foo=bar' })
439462 let resolvedTo : string ;
440463 let resolvedState = state ;
464+
441465 if ( typeof to === 'object' && to !== null ) {
442466 const loc = to as Partial < RouterLocation > ;
443467 // If no pathname provided, use current pathname to stay on current page
444- let path = loc . pathname
468+ resolvedTo = loc . pathname
445469 ? resolvePath ( loc . pathname )
446470 : currentLocation . pathname ;
471+
472+ // Append search and hash directly to the path to preserve the raw
473+ // query string format. TanStack Router's search prop uses JSON
474+ // serialization which is incompatible with standard URL query strings.
447475 if ( loc . search ) {
448- path += loc . search . startsWith ( '?' )
476+ resolvedTo += loc . search . startsWith ( '?' )
449477 ? loc . search
450478 : `?${ loc . search } ` ;
451479 }
452480 if ( loc . hash ) {
453- path += loc . hash . startsWith ( '#' ) ? loc . hash : `#${ loc . hash } ` ;
481+ resolvedTo += loc . hash . startsWith ( '#' )
482+ ? loc . hash
483+ : `#${ loc . hash } ` ;
454484 }
455- resolvedTo = path ;
485+
456486 resolvedState = loc . state || state ;
457487 } else {
458488 resolvedTo = resolvePath ( to as string ) ;
@@ -475,20 +505,30 @@ Link.displayName = 'Link';
475505
476506const Navigate = ( { to, replace, state } : RouterNavigateProps ) => {
477507 const basename = useBasename ( ) ;
508+ const router = useRouter ( ) ;
478509 const currentLocation = useTanStackLocation ( ) ;
479510
480511 // Handle both string and object forms of `to`
481512 let resolvedPath : string ;
482- let search : string | undefined ;
483- let hash : string | undefined ;
484513
485514 if ( typeof to === 'string' ) {
486515 resolvedPath = to ;
487516 } else {
488517 // If no pathname provided, use current pathname to stay on current page
489518 resolvedPath = to . pathname ?? currentLocation . pathname ;
490- search = to . search ;
491- hash = to . hash ;
519+
520+ // Append search and hash directly to the path to preserve the raw
521+ // query string format. TanStack Router's search prop uses JSON
522+ // serialization which is incompatible with standard URL query strings.
523+ if ( to . search ) {
524+ resolvedPath += to . search . startsWith ( '?' )
525+ ? to . search
526+ : `?${ to . search } ` ;
527+ }
528+ if ( to . hash ) {
529+ resolvedPath += to . hash . startsWith ( '#' ) ? to . hash : `#${ to . hash } ` ;
530+ }
531+
492532 // Merge state from object with state prop (prop takes precedence)
493533 state = state ?? to . state ;
494534 }
@@ -504,19 +544,20 @@ const Navigate = ({ to, replace, state }: RouterNavigateProps) => {
504544 }
505545 }
506546
507- return (
508- < TanStackNavigate
509- to = { resolvedPath }
510- search = {
511- search
512- ? Object . fromEntries ( new URLSearchParams ( search ) )
513- : undefined
514- }
515- hash = { hash }
516- replace = { replace }
517- state = { state }
518- />
519- ) ;
547+ // Use TanStack Router's navigate function
548+ const previousPathRef = React . useRef < string | null > ( null ) ;
549+ React . useLayoutEffect ( ( ) => {
550+ if ( previousPathRef . current !== resolvedPath ) {
551+ router . navigate ( {
552+ to : resolvedPath ,
553+ state,
554+ replace,
555+ } ) ;
556+ previousPathRef . current = resolvedPath ;
557+ }
558+ } , [ router , resolvedPath , replace , state ] ) ;
559+
560+ return null ;
520561} ;
521562
522563/**
0 commit comments