Skip to content

Commit 09e152a

Browse files
fix: Make buildLocation aware of non-changing routes (#4573)
This is an attempt to resolve #4526 by making buildLocation aware of the fact that even though there is a "to" path the route is not always changing. This is specifically the case with to="." navigations. This also adds a test largely based on the test provided by [amargielewski](https://github.com/amargielewski) fixes #4526 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 9e35918 commit 09e152a

File tree

2 files changed

+223
-3
lines changed

2 files changed

+223
-3
lines changed

packages/react-router/tests/useNavigate.test.tsx

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,220 @@ test('<Navigate> navigates only once in <StrictMode>', async () => {
13661366
expect(navigateSpy.mock.calls.length).toBe(1)
13671367
})
13681368

1369+
test('should navigate to current route with search params when using "." in nested route structure', async () => {
1370+
const rootRoute = createRootRoute()
1371+
1372+
const IndexComponent = () => {
1373+
const navigate = useNavigate()
1374+
return (
1375+
<>
1376+
<button
1377+
data-testid="posts-btn"
1378+
onClick={() => {
1379+
navigate({
1380+
to: '/post',
1381+
})
1382+
}}
1383+
>
1384+
Post
1385+
</button>
1386+
<button
1387+
data-testid="search-btn"
1388+
onClick={() =>
1389+
navigate({
1390+
to: '.',
1391+
search: {
1392+
param1: 'value1',
1393+
},
1394+
})
1395+
}
1396+
>
1397+
Search
1398+
</button>
1399+
<button
1400+
data-testid="search2-btn"
1401+
onClick={() =>
1402+
navigate({
1403+
to: '/post',
1404+
search: {
1405+
param1: 'value2',
1406+
},
1407+
})
1408+
}
1409+
>
1410+
Search2
1411+
</button>
1412+
<Outlet />
1413+
</>
1414+
)
1415+
}
1416+
1417+
const indexRoute = createRoute({
1418+
getParentRoute: () => rootRoute,
1419+
path: '/',
1420+
component: IndexComponent,
1421+
validateSearch: z.object({
1422+
param1: z.string().optional(),
1423+
}),
1424+
})
1425+
1426+
const postRoute = createRoute({
1427+
getParentRoute: () => indexRoute,
1428+
path: 'post',
1429+
component: () => <div>Post</div>,
1430+
})
1431+
1432+
const router = createRouter({
1433+
routeTree: rootRoute.addChildren([indexRoute, postRoute]),
1434+
history,
1435+
})
1436+
1437+
render(<RouterProvider router={router} />)
1438+
1439+
const postButton = await screen.findByTestId('posts-btn')
1440+
1441+
fireEvent.click(postButton)
1442+
1443+
expect(router.state.location.pathname).toBe('/post')
1444+
1445+
const searchButton = await screen.findByTestId('search-btn')
1446+
1447+
fireEvent.click(searchButton)
1448+
1449+
expect(router.state.location.pathname).toBe('/post')
1450+
expect(router.state.location.search).toEqual({ param1: 'value1' })
1451+
1452+
const searchButton2 = await screen.findByTestId('search2-btn')
1453+
1454+
fireEvent.click(searchButton2)
1455+
1456+
expect(router.state.location.pathname).toBe('/post')
1457+
expect(router.state.location.search).toEqual({ param1: 'value2' })
1458+
})
1459+
1460+
test('should navigate to current route with changing path params when using "." in nested route structure', async () => {
1461+
const rootRoute = createRootRoute()
1462+
1463+
const IndexComponent = () => {
1464+
const navigate = useNavigate()
1465+
return (
1466+
<>
1467+
<h1 data-testid="index-heading">Index</h1>
1468+
<button
1469+
data-testid="posts-btn"
1470+
onClick={() => navigate({ to: '/posts' })}
1471+
>
1472+
Posts
1473+
</button>
1474+
</>
1475+
)
1476+
}
1477+
1478+
const indexRoute = createRoute({
1479+
getParentRoute: () => rootRoute,
1480+
path: '/',
1481+
component: IndexComponent,
1482+
})
1483+
1484+
const layoutRoute = createRoute({
1485+
getParentRoute: () => rootRoute,
1486+
id: '_layout',
1487+
component: () => {
1488+
return (
1489+
<>
1490+
<h1>Layout</h1>
1491+
<Outlet />
1492+
</>
1493+
)
1494+
},
1495+
})
1496+
1497+
const PostsComponent = () => {
1498+
const navigate = postsRoute.useNavigate()
1499+
return (
1500+
<>
1501+
<h1 data-testid="posts-index-heading">Posts</h1>
1502+
<button
1503+
data-testid="first-post-btn"
1504+
onClick={() =>
1505+
navigate({
1506+
to: '$postId',
1507+
params: { postId: 'id1' },
1508+
})
1509+
}
1510+
>
1511+
To first post
1512+
</button>
1513+
<button
1514+
data-testid="second-post-btn"
1515+
onClick={() =>
1516+
navigate({
1517+
to: '.',
1518+
params: { postId: 'id2' },
1519+
})
1520+
}
1521+
>
1522+
To second post
1523+
</button>
1524+
<Outlet />
1525+
</>
1526+
)
1527+
}
1528+
1529+
const postsRoute = createRoute({
1530+
getParentRoute: () => layoutRoute,
1531+
path: 'posts',
1532+
component: PostsComponent,
1533+
})
1534+
1535+
const PostComponent = () => {
1536+
const params = useParams({ strict: false })
1537+
return (
1538+
<>
1539+
<span data-testid={`post-${params.postId}`}>
1540+
Params: {params.postId}
1541+
</span>
1542+
</>
1543+
)
1544+
}
1545+
1546+
const postRoute = createRoute({
1547+
getParentRoute: () => postsRoute,
1548+
path: '$postId',
1549+
component: PostComponent,
1550+
})
1551+
1552+
const router = createRouter({
1553+
routeTree: rootRoute.addChildren([
1554+
indexRoute,
1555+
layoutRoute.addChildren([postsRoute.addChildren([postRoute])]),
1556+
]),
1557+
})
1558+
1559+
render(<RouterProvider router={router} />)
1560+
1561+
const postsButton = await screen.findByTestId('posts-btn')
1562+
1563+
fireEvent.click(postsButton)
1564+
1565+
expect(await screen.findByTestId('posts-index-heading')).toBeInTheDocument()
1566+
expect(window.location.pathname).toEqual('/posts')
1567+
1568+
const firstPostButton = await screen.findByTestId('first-post-btn')
1569+
1570+
fireEvent.click(firstPostButton)
1571+
1572+
expect(await screen.findByTestId('post-id1')).toBeInTheDocument()
1573+
expect(window.location.pathname).toEqual('/posts/id1')
1574+
1575+
const secondPostButton = await screen.findByTestId('second-post-btn')
1576+
1577+
fireEvent.click(secondPostButton)
1578+
1579+
expect(await screen.findByTestId('post-id2')).toBeInTheDocument()
1580+
expect(window.location.pathname).toEqual('/posts/id2')
1581+
})
1582+
13691583
describe('when on /posts/$postId and navigating to ../ with default `from` /posts', () => {
13701584
async function runTest(navigateVia: 'Route' | 'RouteApi') {
13711585
const rootRoute = createRootRoute()

packages/router-core/src/router.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,11 +1415,15 @@ export class RouterCore<
14151415
// By default, start with the current location
14161416
let fromPath = lastMatch.fullPath
14171417

1418-
// If there is a to, it means we are changing the path in some way
1419-
// So we need to find the relative fromPath
1418+
const routeIsChanging =
1419+
!!dest.to &&
1420+
dest.to !== fromPath &&
1421+
this.resolvePathWithBase(fromPath, `${dest.to}`) !== fromPath
1422+
1423+
// If the route is changing we need to find the relative fromPath
14201424
if (dest.unsafeRelative === 'path') {
14211425
fromPath = currentLocation.pathname
1422-
} else if (dest.to && dest.from) {
1426+
} else if (routeIsChanging && dest.from) {
14231427
fromPath = dest.from
14241428
const existingFrom = [...allFromMatches].reverse().find((d) => {
14251429
return (
@@ -1708,6 +1712,7 @@ export class RouterCore<
17081712
}: BuildNextOptions & CommitLocationOptions = {}) => {
17091713
if (href) {
17101714
const currentIndex = this.history.location.state.__TSR_index
1715+
17111716
const parsed = parseHref(href, {
17121717
__TSR_index: replace ? currentIndex : currentIndex + 1,
17131718
})
@@ -1721,6 +1726,7 @@ export class RouterCore<
17211726
...(rest as any),
17221727
_includeValidateSearch: true,
17231728
})
1729+
17241730
return this.commitLocation({
17251731
...location,
17261732
viewTransition,

0 commit comments

Comments
 (0)