Skip to content

Commit e665a46

Browse files
authored
Fix basename duplication in RouterProvider descendant routes (#10492)
1 parent 55482ec commit e665a46

File tree

3 files changed

+194
-5
lines changed

3 files changed

+194
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Fix `basename` duplication in descenant `<Routes>` inside a `<RouterProvider>`

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

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2053,6 +2053,181 @@ describe("useNavigate", () => {
20532053
`);
20542054
});
20552055
});
2056+
2057+
describe("with a basename", () => {
2058+
describe("in a MemoryRouter", () => {
2059+
it("in a root route", () => {
2060+
let renderer: TestRenderer.ReactTestRenderer;
2061+
TestRenderer.act(() => {
2062+
renderer = TestRenderer.create(
2063+
<MemoryRouter basename="/base" initialEntries={["/base"]}>
2064+
<Routes>
2065+
<Route path="/" element={<Home />} />
2066+
<Route path="/path" element={<h1>Path</h1>} />
2067+
</Routes>
2068+
</MemoryRouter>
2069+
);
2070+
});
2071+
2072+
function Home() {
2073+
let navigate = useNavigate();
2074+
return <button onClick={() => navigate("/path")} />;
2075+
}
2076+
2077+
// @ts-expect-error
2078+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
2079+
<button
2080+
onClick={[Function]}
2081+
/>
2082+
`);
2083+
2084+
// @ts-expect-error
2085+
let button = renderer.root.findByType("button");
2086+
TestRenderer.act(() => button.props.onClick());
2087+
2088+
// @ts-expect-error
2089+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
2090+
<h1>
2091+
Path
2092+
</h1>
2093+
`);
2094+
});
2095+
2096+
it("in a descendant route", () => {
2097+
let renderer: TestRenderer.ReactTestRenderer;
2098+
TestRenderer.act(() => {
2099+
renderer = TestRenderer.create(
2100+
<MemoryRouter basename="/base" initialEntries={["/base"]}>
2101+
<Routes>
2102+
<Route
2103+
path="/*"
2104+
element={
2105+
<Routes>
2106+
<Route index element={<Home />} />
2107+
</Routes>
2108+
}
2109+
/>
2110+
<Route path="/path" element={<h1>Path</h1>} />
2111+
</Routes>
2112+
</MemoryRouter>
2113+
);
2114+
});
2115+
2116+
function Home() {
2117+
let navigate = useNavigate();
2118+
return <button onClick={() => navigate("/path")} />;
2119+
}
2120+
2121+
// @ts-expect-error
2122+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
2123+
<button
2124+
onClick={[Function]}
2125+
/>
2126+
`);
2127+
2128+
// @ts-expect-error
2129+
let button = renderer.root.findByType("button");
2130+
TestRenderer.act(() => button.props.onClick());
2131+
2132+
// @ts-expect-error
2133+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
2134+
<h1>
2135+
Path
2136+
</h1>
2137+
`);
2138+
});
2139+
});
2140+
2141+
describe("in a RouterProvider", () => {
2142+
it("in a root route", () => {
2143+
let router = createMemoryRouter(
2144+
[
2145+
{
2146+
path: "/",
2147+
Component: Home,
2148+
},
2149+
{ path: "/path", Component: () => <h1>Path</h1> },
2150+
],
2151+
{ basename: "/base", initialEntries: ["/base"] }
2152+
);
2153+
2154+
function Home() {
2155+
let navigate = useNavigate();
2156+
return <button onClick={() => navigate("/path")} />;
2157+
}
2158+
2159+
let renderer: TestRenderer.ReactTestRenderer;
2160+
TestRenderer.act(() => {
2161+
renderer = TestRenderer.create(<RouterProvider router={router} />);
2162+
});
2163+
2164+
// @ts-expect-error
2165+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
2166+
<button
2167+
onClick={[Function]}
2168+
/>
2169+
`);
2170+
2171+
// @ts-expect-error
2172+
let button = renderer.root.findByType("button");
2173+
TestRenderer.act(() => button.props.onClick());
2174+
2175+
// @ts-expect-error
2176+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
2177+
<h1>
2178+
Path
2179+
</h1>
2180+
`);
2181+
});
2182+
2183+
it("in a descendant route", () => {
2184+
let router = createMemoryRouter(
2185+
[
2186+
{
2187+
path: "/*",
2188+
Component() {
2189+
return (
2190+
<Routes>
2191+
<Route index element={<Home />} />
2192+
</Routes>
2193+
);
2194+
},
2195+
},
2196+
{ path: "/path", Component: () => <h1>Path</h1> },
2197+
],
2198+
{ basename: "/base", initialEntries: ["/base"] }
2199+
);
2200+
2201+
function Home() {
2202+
let navigate = useNavigate();
2203+
return <button onClick={() => navigate("/path")} />;
2204+
}
2205+
2206+
let renderer: TestRenderer.ReactTestRenderer;
2207+
TestRenderer.act(() => {
2208+
renderer = TestRenderer.create(<RouterProvider router={router} />);
2209+
});
2210+
2211+
// @ts-expect-error
2212+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
2213+
<button
2214+
onClick={[Function]}
2215+
/>
2216+
`);
2217+
2218+
// @ts-expect-error
2219+
let button = renderer.root.findByType("button");
2220+
TestRenderer.act(() => button.props.onClick());
2221+
2222+
// @ts-expect-error
2223+
expect(renderer.toJSON()).toMatchInlineSnapshot(`
2224+
<h1>
2225+
Path
2226+
</h1>
2227+
`);
2228+
});
2229+
});
2230+
});
20562231
});
20572232

20582233
function UseNavigateButton({

packages/react-router/lib/hooks.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ function useNavigateUnstable(): NavigateFunction {
188188
`useNavigate() may be used only in the context of a <Router> component.`
189189
);
190190

191+
let dataRouterContext = React.useContext(DataRouterContext);
191192
let { basename, navigator } = React.useContext(NavigationContext);
192193
let { matches } = React.useContext(RouteContext);
193194
let { pathname: locationPathname } = useLocation();
@@ -222,10 +223,12 @@ function useNavigateUnstable(): NavigateFunction {
222223
);
223224

224225
// If we're operating within a basename, prepend it to the pathname prior
225-
// to handing off to history. If this is a root navigation, then we
226-
// navigate to the raw basename which allows the basename to have full
227-
// control over the presence of a trailing slash on root links
228-
if (basename !== "/") {
226+
// to handing off to history (but only if we're not in a data router,
227+
// otherwise it'll prepend the basename inside of the router).
228+
// If this is a root navigation, then we navigate to the raw basename
229+
// which allows the basename to have full control over the presence of a
230+
// trailing slash on root links
231+
if (dataRouterContext == null && basename !== "/") {
229232
path.pathname =
230233
path.pathname === "/"
231234
? basename
@@ -238,7 +241,13 @@ function useNavigateUnstable(): NavigateFunction {
238241
options
239242
);
240243
},
241-
[basename, navigator, routePathnamesJson, locationPathname]
244+
[
245+
basename,
246+
navigator,
247+
routePathnamesJson,
248+
locationPathname,
249+
dataRouterContext,
250+
]
242251
);
243252

244253
return navigate;

0 commit comments

Comments
 (0)