Skip to content

Commit 38e1bb4

Browse files
committed
Avoid changing the order of search parameters
1 parent 5fd97be commit 38e1bb4

1 file changed

Lines changed: 84 additions & 43 deletions

File tree

packages/ra-router-tanstack/src/tanStackRouterProvider.tsx

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import * as React from 'react';
22
import { ReactNode, forwardRef, useCallback, useMemo } from 'react';
33
import {
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

261259
const 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+
364376
const 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

476506
const 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

Comments
 (0)