Skip to content

Commit f9652c6

Browse files
authored
fix: support basename in static routers (#9591)
1 parent fd4383d commit f9652c6

File tree

7 files changed

+154
-18
lines changed

7 files changed

+154
-18
lines changed

.changeset/funny-oranges-arrive.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"react-router-dom": patch
3+
"@remix-run/router": patch
4+
---
5+
6+
Support `basename` in static data routers

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
},
108108
"filesize": {
109109
"packages/router/dist/router.umd.min.js": {
110-
"none": "34.5 kB"
110+
"none": "35 kB"
111111
},
112112
"packages/react-router/dist/react-router.production.min.js": {
113113
"none": "12.5 kB"

packages/react-router-dom/__tests__/data-static-router-test.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as ReactDOMServer from "react-dom/server";
33
import type { StaticHandlerContext } from "@remix-run/router";
44
import { unstable_createStaticHandler as createStaticHandler } from "@remix-run/router";
55
import {
6+
Link,
67
Outlet,
78
useLoaderData,
89
useLocation,
@@ -17,7 +18,7 @@ beforeEach(() => {
1718
jest.spyOn(console, "warn").mockImplementation(() => {});
1819
});
1920

20-
describe("A <DataStaticRouter>", () => {
21+
describe("A <StaticRouterProvider>", () => {
2122
it("renders an initialized router", async () => {
2223
let hooksData1: {
2324
location: ReturnType<typeof useLocation>;
@@ -45,7 +46,12 @@ describe("A <DataStaticRouter>", () => {
4546
loaderData: useLoaderData(),
4647
matches: useMatches(),
4748
};
48-
return <h1>👋</h1>;
49+
return (
50+
<>
51+
<h1>👋</h1>
52+
<Link to="/the/other/path">Other</Link>
53+
</>
54+
);
4955
}
5056

5157
let routes = [
@@ -71,7 +77,7 @@ describe("A <DataStaticRouter>", () => {
7177
let { query } = createStaticHandler(routes);
7278

7379
let context = (await query(
74-
new Request("http:/localhost/the/path?the=query#the-hash", {
80+
new Request("http://localhost/the/path?the=query#the-hash", {
7581
signal: new AbortController().signal,
7682
})
7783
)) as StaticHandlerContext;
@@ -85,6 +91,7 @@ describe("A <DataStaticRouter>", () => {
8591
</React.StrictMode>
8692
);
8793
expect(html).toMatch("<h1>👋</h1>");
94+
expect(html).toMatch('<a href="/the/other/path">');
8895

8996
// @ts-expect-error
9097
expect(hooksData1.location).toEqual({
@@ -155,6 +162,59 @@ describe("A <DataStaticRouter>", () => {
155162
]);
156163
});
157164

165+
it("renders an initialized router with a basename", async () => {
166+
let location: ReturnType<typeof useLocation>;
167+
168+
function GetLocation() {
169+
location = useLocation();
170+
return (
171+
<>
172+
<h1>👋</h1>
173+
<Link to="/the/other/path">Other</Link>
174+
</>
175+
);
176+
}
177+
178+
let routes = [
179+
{
180+
path: "the",
181+
children: [
182+
{
183+
path: "path",
184+
element: <GetLocation />,
185+
},
186+
],
187+
},
188+
];
189+
let { query } = createStaticHandler(routes, { basename: "/base" });
190+
191+
let context = (await query(
192+
new Request("http://localhost/base/the/path?the=query#the-hash", {
193+
signal: new AbortController().signal,
194+
})
195+
)) as StaticHandlerContext;
196+
197+
let html = ReactDOMServer.renderToStaticMarkup(
198+
<React.StrictMode>
199+
<StaticRouterProvider
200+
router={createStaticRouter(routes, context)}
201+
context={context}
202+
/>
203+
</React.StrictMode>
204+
);
205+
expect(html).toMatch("<h1>👋</h1>");
206+
expect(html).toMatch('<a href="/base/the/other/path">');
207+
208+
// @ts-expect-error
209+
expect(location).toEqual({
210+
pathname: "/the/path",
211+
search: "?the=query",
212+
hash: "#the-hash",
213+
state: null,
214+
key: expect.any(String),
215+
});
216+
});
217+
158218
it("renders hydration data by default", async () => {
159219
let routes = [
160220
{

packages/react-router-dom/__tests__/static-link-test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ describe("A <Link> in a <StaticRouter>", () => {
1717

1818
expect(renderer.root.findByType("a").props.href).toEqual("/mjackson");
1919
});
20+
21+
it("uses the right href with a basename", () => {
22+
let renderer: TestRenderer.ReactTestRenderer;
23+
TestRenderer.act(() => {
24+
renderer = TestRenderer.create(
25+
<StaticRouter location="/base" basename="/base">
26+
<Link to="mjackson" />
27+
</StaticRouter>
28+
);
29+
});
30+
31+
expect(renderer.root.findByType("a").props.href).toEqual(
32+
"/base/mjackson"
33+
);
34+
});
2035
});
2136

2237
describe("with an object", () => {

packages/react-router-dom/server.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ export function StaticRouter({
6565
}
6666

6767
export interface StaticRouterProviderProps {
68-
basename?: string;
6968
context: StaticHandlerContext;
7069
router: RemixRouter;
7170
hydrate?: boolean;
@@ -77,7 +76,6 @@ export interface StaticRouterProviderProps {
7776
* on the server where there is no stateful UI.
7877
*/
7978
export function unstable_StaticRouterProvider({
80-
basename,
8179
context,
8280
router,
8381
hydrate = true,
@@ -92,7 +90,7 @@ export function unstable_StaticRouterProvider({
9290
router,
9391
navigator: getStatelessNavigator(),
9492
static: true,
95-
basename: basename || "/",
93+
basename: context.basename || "/",
9694
};
9795

9896
let hydrateScript = "";
@@ -191,7 +189,7 @@ export function unstable_createStaticRouter(
191189

192190
return {
193191
get basename() {
194-
return "/";
192+
return context.basename;
195193
},
196194
get state() {
197195
return {

packages/router/__tests__/router-test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10100,6 +10100,21 @@ describe("a router", () => {
1010010100
});
1010110101
});
1010210102

10103+
it("should support document load navigations with a basename", async () => {
10104+
let { query } = createStaticHandler(SSR_ROUTES, { basename: "/base" });
10105+
let context = await query(createRequest("/base/parent/child"));
10106+
expect(context).toMatchObject({
10107+
actionData: null,
10108+
loaderData: {
10109+
parent: "PARENT LOADER",
10110+
child: "CHILD LOADER",
10111+
},
10112+
errors: null,
10113+
location: { pathname: "/base/parent/child" },
10114+
matches: [{ route: { id: "parent" } }, { route: { id: "child" } }],
10115+
});
10116+
});
10117+
1010310118
it("should support document load navigations returning responses", async () => {
1010410119
let { query } = createStaticHandler(SSR_ROUTES);
1010510120
let context = await query(createRequest("/parent/json"));
@@ -11020,6 +11035,38 @@ describe("a router", () => {
1102011035
expect(data).toBe("");
1102111036
});
1102211037

11038+
it("should support singular route load navigations (with a basename)", async () => {
11039+
let { queryRoute } = createStaticHandler(SSR_ROUTES, {
11040+
basename: "/base",
11041+
});
11042+
let data;
11043+
11044+
// Layout route
11045+
data = await queryRoute(createRequest("/base/parent"), "parent");
11046+
expect(data).toBe("PARENT LOADER");
11047+
11048+
// Index route
11049+
data = await queryRoute(createRequest("/base/parent"), "parentIndex");
11050+
expect(data).toBe("PARENT INDEX LOADER");
11051+
11052+
// Parent in nested route
11053+
data = await queryRoute(createRequest("/base/parent/child"), "parent");
11054+
expect(data).toBe("PARENT LOADER");
11055+
11056+
// Child in nested route
11057+
data = await queryRoute(createRequest("/base/parent/child"), "child");
11058+
expect(data).toBe("CHILD LOADER");
11059+
11060+
// Non-undefined falsey values should count
11061+
let T = setupFlexRouteTest();
11062+
data = await T.resolveLoader(null);
11063+
expect(data).toBeNull();
11064+
data = await T.resolveLoader(false);
11065+
expect(data).toBe(false);
11066+
data = await T.resolveLoader("");
11067+
expect(data).toBe("");
11068+
});
11069+
1102311070
it("should support singular route submit navigations (primitives)", async () => {
1102411071
let { queryRoute } = createStaticHandler(SSR_ROUTES);
1102511072
let data;

packages/router/router.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ export interface RouterInit {
298298
* State returned from a server-side query() call
299299
*/
300300
export interface StaticHandlerContext {
301+
basename: Router["basename"];
301302
location: RouterState["location"];
302303
matches: RouterState["matches"];
303304
loaderData: RouterState["loaderData"];
@@ -1858,14 +1859,18 @@ const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
18581859
const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
18591860

18601861
export function unstable_createStaticHandler(
1861-
routes: AgnosticRouteObject[]
1862+
routes: AgnosticRouteObject[],
1863+
opts?: {
1864+
basename?: string;
1865+
}
18621866
): StaticHandler {
18631867
invariant(
18641868
routes.length > 0,
18651869
"You must provide a non-empty routes array to unstable_createStaticHandler"
18661870
);
18671871

18681872
let dataRoutes = convertRoutesToDataRoutes(routes);
1873+
let basename = (opts ? opts.basename : null) || "/";
18691874

18701875
/**
18711876
* The query() method is intended for document requests, in which we want to
@@ -1891,13 +1896,14 @@ export function unstable_createStaticHandler(
18911896
): Promise<StaticHandlerContext | Response> {
18921897
let url = new URL(request.url);
18931898
let location = createLocation("", createPath(url), null, "default");
1894-
let matches = matchRoutes(dataRoutes, location);
1899+
let matches = matchRoutes(dataRoutes, location, basename);
18951900

18961901
if (!validRequestMethods.has(request.method)) {
18971902
let error = getInternalRouterError(405, { method: request.method });
18981903
let { matches: methodNotAllowedMatches, route } =
18991904
getShortCircuitMatches(dataRoutes);
19001905
return {
1906+
basename,
19011907
location,
19021908
matches: methodNotAllowedMatches,
19031909
loaderData: {},
@@ -1914,6 +1920,7 @@ export function unstable_createStaticHandler(
19141920
let { matches: notFoundMatches, route } =
19151921
getShortCircuitMatches(dataRoutes);
19161922
return {
1923+
basename,
19171924
location,
19181925
matches: notFoundMatches,
19191926
loaderData: {},
@@ -1935,7 +1942,7 @@ export function unstable_createStaticHandler(
19351942
// When returning StaticHandlerContext, we patch back in the location here
19361943
// since we need it for React Context. But this helps keep our submit and
19371944
// loadRouteData operating on a Request instead of a Location
1938-
return { location, ...result };
1945+
return { location, basename, ...result };
19391946
}
19401947

19411948
/**
@@ -1961,7 +1968,7 @@ export function unstable_createStaticHandler(
19611968
async function queryRoute(request: Request, routeId?: string): Promise<any> {
19621969
let url = new URL(request.url);
19631970
let location = createLocation("", createPath(url), null, "default");
1964-
let matches = matchRoutes(dataRoutes, location);
1971+
let matches = matchRoutes(dataRoutes, location, basename);
19651972

19661973
if (!validRequestMethods.has(request.method)) {
19671974
throw getInternalRouterError(405, { method: request.method });
@@ -2007,7 +2014,7 @@ export function unstable_createStaticHandler(
20072014
location: Location,
20082015
matches: AgnosticDataRouteMatch[],
20092016
routeMatch?: AgnosticDataRouteMatch
2010-
): Promise<Omit<StaticHandlerContext, "location"> | Response> {
2017+
): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
20112018
invariant(
20122019
request.signal,
20132020
"query()/queryRoute() requests must contain an AbortController signal"
@@ -2056,7 +2063,7 @@ export function unstable_createStaticHandler(
20562063
matches: AgnosticDataRouteMatch[],
20572064
actionMatch: AgnosticDataRouteMatch,
20582065
isRouteRequest: boolean
2059-
): Promise<Omit<StaticHandlerContext, "location"> | Response> {
2066+
): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
20602067
let result: DataResult;
20612068

20622069
if (!actionMatch.route.action) {
@@ -2078,7 +2085,7 @@ export function unstable_createStaticHandler(
20782085
request,
20792086
actionMatch,
20802087
matches,
2081-
undefined, // Basename not currently supported in static handlers
2088+
basename,
20822089
true,
20832090
isRouteRequest
20842091
);
@@ -2168,7 +2175,10 @@ export function unstable_createStaticHandler(
21682175
routeMatch?: AgnosticDataRouteMatch,
21692176
pendingActionError?: RouteData
21702177
): Promise<
2171-
| Omit<StaticHandlerContext, "location" | "actionData" | "actionHeaders">
2178+
| Omit<
2179+
StaticHandlerContext,
2180+
"location" | "basename" | "actionData" | "actionHeaders"
2181+
>
21722182
| Response
21732183
> {
21742184
let isRouteRequest = routeMatch != null;
@@ -2208,7 +2218,7 @@ export function unstable_createStaticHandler(
22082218
request,
22092219
match,
22102220
matches,
2211-
undefined, // Basename not currently supported in static handlers
2221+
basename,
22122222
true,
22132223
isRouteRequest
22142224
)
@@ -2519,7 +2529,7 @@ async function callLoaderOrAction(
25192529
request: Request,
25202530
match: AgnosticDataRouteMatch,
25212531
matches: AgnosticDataRouteMatch[],
2522-
basename: string | undefined,
2532+
basename = "/",
25232533
isStaticRequest: boolean = false,
25242534
isRouteRequest: boolean = false
25252535
): Promise<DataResult> {

0 commit comments

Comments
 (0)