diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx
index bc05752fe4..8261c90cfb 100644
--- a/packages/react-router-dom/index.tsx
+++ b/packages/react-router-dom/index.tsx
@@ -81,7 +81,6 @@ export type {
Navigator,
OutletProps,
Params,
- PartialRouteObject,
PathMatch,
RouteMatch,
RouteObject,
diff --git a/packages/react-router/__tests__/absolute-path-matching-test.tsx b/packages/react-router/__tests__/absolute-path-matching-test.tsx
new file mode 100644
index 0000000000..a3c4a21e70
--- /dev/null
+++ b/packages/react-router/__tests__/absolute-path-matching-test.tsx
@@ -0,0 +1,4 @@
+describe("absolute paths", () => {
+ it.todo("matches when the path matches");
+ it.todo("throws when the nested path does not begin with its parent path");
+});
diff --git a/packages/react-router/__tests__/index-routes-test.tsx b/packages/react-router/__tests__/index-routes-test.tsx
new file mode 100644
index 0000000000..256a6780e0
--- /dev/null
+++ b/packages/react-router/__tests__/index-routes-test.tsx
@@ -0,0 +1,3 @@
+describe("index routes", () => {
+ it.todo("throws when the index route has children");
+});
diff --git a/packages/react-router/__tests__/path-matching-test.tsx b/packages/react-router/__tests__/path-matching-test.tsx
index 97a04515fd..7e11df17a0 100644
--- a/packages/react-router/__tests__/path-matching-test.tsx
+++ b/packages/react-router/__tests__/path-matching-test.tsx
@@ -1,5 +1,5 @@
-import { matchRoutes } from "react-router";
import type { PartialRouteObject } from "react-router";
+import { matchRoutes } from "react-router";
describe("path matching", () => {
function pickPaths(routes: PartialRouteObject[], pathname: string) {
@@ -71,7 +71,7 @@ describe("path matching", () => {
children: [{ path: "subjects" }]
},
{ path: "new" },
- { path: "/" },
+ { index: true },
{ path: "*" }
]
},
@@ -87,7 +87,7 @@ describe("path matching", () => {
{ path: "*" }
];
- expect(pickPaths(routes, "/courses")).toEqual(["courses", "/"]);
+ expect(pickPaths(routes, "/courses")).toEqual(["courses", ""]);
expect(pickPaths(routes, "/courses/routing")).toEqual(["courses", ":id"]);
expect(pickPaths(routes, "/courses/routing/subjects")).toEqual([
"courses",
@@ -115,7 +115,7 @@ describe("path matching", () => {
let routes = [
{
path: ":page",
- children: [{ path: "/" }]
+ children: [{ index: true }]
},
{ path: "page" }
];
@@ -125,7 +125,7 @@ describe("path matching", () => {
});
describe("path matching with a basename", () => {
- let routes = [
+ let routes: PartialRouteObject[] = [
{
path: "/users/:userId",
children: [
diff --git a/packages/react-router/__tests__/route-depth-order-matching-test.tsx b/packages/react-router/__tests__/route-depth-order-matching-test.tsx
index 4b8f13cc17..7bfc6f60ef 100644
--- a/packages/react-router/__tests__/route-depth-order-matching-test.tsx
+++ b/packages/react-router/__tests__/route-depth-order-matching-test.tsx
@@ -3,6 +3,57 @@ import { act, create as createTestRenderer } from "react-test-renderer";
import { MemoryRouter as Router, Outlet, Routes, Route } from "react-router";
import type { ReactTestRenderer } from "react-test-renderer";
+describe("nested routes with no path", () => {
+ it("matches them depth-first", () => {
+ let renderer!: ReactTestRenderer;
+ act(() => {
+ renderer = createTestRenderer(
+
+
+ }>
+ }>
+ } />
+
+
+
+
+ );
+ });
+
+ expect(renderer.toJSON()).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ function First() {
+ return (
+
+ First
+
+ );
+ }
+
+ function Second() {
+ return (
+
+ Second
+
+ );
+ }
+
+ function Third() {
+ return Third
;
+ }
+});
+
describe("nested /", () => {
it("matches them depth-first", () => {
let renderer!: ReactTestRenderer;
diff --git a/packages/react-router/__tests__/route-matching-test.tsx b/packages/react-router/__tests__/route-matching-test.tsx
index 3f5851435b..cfae042b92 100644
--- a/packages/react-router/__tests__/route-matching-test.tsx
+++ b/packages/react-router/__tests__/route-matching-test.tsx
@@ -1,5 +1,6 @@
import * as React from "react";
import { create as createTestRenderer } from "react-test-renderer";
+import type { PartialRouteObject } from "react-router";
import {
MemoryRouter as Router,
Outlet,
@@ -8,7 +9,6 @@ import {
useParams,
useRoutes
} from "react-router";
-import type { PartialRouteObject } from "react-router";
import type { InitialEntry } from "history";
describe("route matching", () => {
@@ -28,7 +28,7 @@ describe("route matching", () => {
children: [{ path: "grades", element: }]
},
{ path: "new", element: },
- { path: "/", element: },
+ { index: true, element: },
{ path: "*", element: }
]
},
@@ -56,7 +56,7 @@ describe("route matching", () => {
} />
} />
- } />
+ } />
} />
}>
@@ -80,7 +80,7 @@ describe("route matching", () => {
-
+
@@ -193,4 +193,5 @@ describe("route matching", () => {
interface Props {
children?: React.ReactNode;
path?: string;
+ index?: boolean;
}
diff --git a/packages/react-router/__tests__/useRoutes-test.tsx b/packages/react-router/__tests__/useRoutes-test.tsx
index d1fe6f5c2f..fe38b69294 100644
--- a/packages/react-router/__tests__/useRoutes-test.tsx
+++ b/packages/react-router/__tests__/useRoutes-test.tsx
@@ -1,9 +1,9 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { create as createTestRenderer } from "react-test-renderer";
+import type { RouteObject } from "react-router";
import { MemoryRouter as Router, useRoutes } from "react-router";
import { act } from "react-dom/test-utils";
-import type { PartialRouteObject } from "react-router";
describe("useRoutes", () => {
it("returns the matching element from a route config", () => {
@@ -59,7 +59,7 @@ function RoutesRenderer({
basename,
location
}: {
- routes: PartialRouteObject[];
+ routes: Partial[];
basename?: string;
location?: Partial & { pathname: string };
}) {
diff --git a/packages/react-router/index.tsx b/packages/react-router/index.tsx
index 10fba67385..098b26e4e8 100644
--- a/packages/react-router/index.tsx
+++ b/packages/react-router/index.tsx
@@ -195,6 +195,7 @@ export interface RouteProps {
caseSensitive?: boolean;
children?: React.ReactNode;
element?: React.ReactElement | null;
+ index?: boolean;
path?: string;
}
@@ -528,13 +529,34 @@ function useRoutes_(
// You won't get a warning about 2 different under a
// without a trailing *, but this is a best-effort warning anyway since we
// cannot even give the warning unless they land at the parent route.
+ //
+ // Example:
+ //
+ //
+ // {/* This route path MUST end with /* because otherwise
+ // it will never match /blog/post/123 */}
+ // } />
+ // } />
+ //
+ //
+ // function Blog() {
+ // return (
+ // <>
+ // 123
+ //
+ // } />
+ //
+ // >
+ // )
+ // }
+ // function Post() { ... }
let parentPath = parentRoute && parentRoute.path;
warningOnce(
parentPathname,
!parentRoute || parentRoute.path.endsWith("*"),
`You rendered descendant (or called \`useRoutes\`) at ` +
`"${parentPathname}" (under ) but the ` +
- `parent route path has no trailing "*". This means if you navigate ` +
+ `parent route path has no trailing "/*". This means if you navigate ` +
`deeper, the parent won't match anymore and therefore the child ` +
`routes will never render.\n\n` +
`Please change the parent to matchRoutes(routes, location, basenameForMatching),
+ () => matchRoutes_(routes, location, basenameForMatching),
[location, routes, basenameForMatching]
);
if (!matches) {
- // TODO: Warn about nothing matching, suggest using a catch-all route.
+ if (__DEV__) {
+ // TODO: Warn about nothing matching, suggest using a catch-all route.
+ }
return null;
}
@@ -595,8 +619,10 @@ export function createRoutesFromArray(
): RouteObject[] {
return array.map(partialRoute => {
let route: RouteObject = {
- path: partialRoute.path || "/",
+ path: partialRoute.path || "",
caseSensitive: partialRoute.caseSensitive === true,
+ index: partialRoute.index === true,
+ // This is the same as default value of
element: partialRoute.element ||
};
@@ -637,8 +663,9 @@ export function createRoutesFromChildren(
}
let route: RouteObject = {
- path: element.props.path || "/",
+ path: element.props.path || "",
caseSensitive: element.props.caseSensitive === true,
+ index: element.props.index === true,
// Default behavior is to just render the element that was given. This
// permits people to use any element they prefer, not just (though
// all our official examples and docs use for clarity).
@@ -670,6 +697,7 @@ export type Params = Record;
export interface RouteObject {
caseSensitive: boolean;
children?: RouteObject[];
+ index: boolean;
element: React.ReactNode;
path: string;
}
@@ -679,11 +707,9 @@ export interface RouteObject {
* certain properties of a real route object such as `path` and `element`,
* which have reasonable defaults.
*/
-export interface PartialRouteObject {
- caseSensitive?: boolean;
+export interface PartialRouteObject
+ extends Omit, "children"> {
children?: PartialRouteObject[];
- element?: React.ReactNode;
- path?: string;
}
/**
@@ -716,6 +742,14 @@ export function matchRoutes(
location = parsePath(location);
}
+ return matchRoutes_(createRoutesFromArray(routes), location, basename);
+}
+
+function matchRoutes_(
+ routes: RouteObject[],
+ location: Partial,
+ basename = ""
+): RouteMatch[] | null {
let pathname = location.pathname || "/";
if (basename) {
let base = basename.replace(/^\/*/, "/").replace(/\/+$/, "");
@@ -746,28 +780,41 @@ export interface RouteMatch {
}
function flattenRoutes(
- routes: PartialRouteObject[],
+ routes: RouteObject[],
branches: RouteBranch[] = [],
parentPath = "",
parentRoutes: RouteObject[] = [],
parentIndexes: number[] = []
): RouteBranch[] {
- (routes as RouteObject[]).forEach((route, index) => {
- route = {
- ...route,
- path: route.path || "/",
- caseSensitive: !!route.caseSensitive,
- element: route.element
- };
+ routes.forEach((route, index) => {
+ let path: string;
+ if (route.path.startsWith("/")) {
+ invariant(
+ parentPath && !route.path.startsWith(parentPath),
+ `Absolute must begin ` +
+ `with its parent path "${parentPath}", otherwise it ` +
+ `will be unreachable.`
+ );
+
+ path = route.path;
+ } else {
+ path = joinPaths([parentPath, route.path]);
+ }
- let path = joinPaths([parentPath, route.path]);
let routes = parentRoutes.concat(route);
let indexes = parentIndexes.concat(index);
// Add the children before adding this route to the array so we traverse the
// route tree depth-first and child routes appear before their parents in
// the "flattened" version.
- if (route.children) {
+ if (route.children && route.children.length > 0) {
+ invariant(
+ !route.index,
+ `Index route for must ` +
+ `not have children. If you want the route to be a layout ` +
+ `for its child routes, remove the \`index\` prop.`
+ );
+
flattenRoutes(route.children, branches, path, routes, indexes);
}
@@ -785,9 +832,7 @@ function rankRouteBranches(branches: RouteBranch[]): void {
return memo;
}, {});
- // Sorting is stable in modern browsers, but we still support IE 11, so we
- // need this little helper.
- stableSort(branches, (a, b) => {
+ branches.sort((a, b) => {
let [aPath, , aIndexes] = a;
let aScore = pathScores[aPath];
@@ -843,13 +888,6 @@ function compareIndexes(a: number[], b: number[]): number {
0;
}
-function stableSort(array: any[], compareItems: (a: any, b: any) => number) {
- // This copy lets us get the original index of an item so we can preserve the
- // original ordering in the case that they sort equally.
- let copy = array.slice(0);
- array.sort((a, b) => compareItems(a, b) || copy.indexOf(a) - copy.indexOf(b));
-}
-
function matchRouteBranch(
branch: RouteBranch,
pathname: string