From 30c17c82adb523cbf47a9fb2206bfaafbff3b716 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 25 Jan 2024 16:19:43 -0500 Subject: [PATCH 1/6] POC for nested descendant absolute paths --- .../descendant-routes-splat-matching-test.tsx | 178 +++++++++++++++++- packages/react-router/lib/components.tsx | 4 +- packages/react-router/lib/hooks.tsx | 10 +- 3 files changed, 180 insertions(+), 12 deletions(-) diff --git a/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx b/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx index a5fce5a781..ae6554ccd4 100644 --- a/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx +++ b/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx @@ -1,6 +1,14 @@ import * as React from "react"; import * as TestRenderer from "react-test-renderer"; -import { MemoryRouter, Outlet, Routes, Route, useParams } from "react-router"; +import { + MemoryRouter, + Outlet, + Routes, + Route, + useParams, + createRoutesFromElements, + useRoutes, +} from "react-router"; import type { InitialEntry } from "@remix-run/router"; describe("Descendant splat matching", () => { @@ -58,9 +66,161 @@ describe("Descendant splat matching", () => { `); }); + + describe("/useRoutes absolute config", () => { + it(" treats descendant route leading-slash paths as relative by default", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + + + ); + }); + + function Auth() { + return ( + + Auth Login} /> + Not Found} /> + + ); + } + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +

+ Not Found +

+ `); + + let renderer2: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer2 = TestRenderer.create( + + + } /> + + + ); + }); + + expect(renderer2.toJSON()).toMatchInlineSnapshot(` +

+ Auth Login +

+ `); + }); + + it(" treats descendant route leading-slash paths as absolute when specified", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + + + ); + }); + + function Auth() { + return ( + + Auth Login} /> + Not Found} /> + + ); + } + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +

+ Auth Login +

+ `); + }); + + it("useRoutes() treats descendant route leading-slash paths as relative by default", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + + + ); + }); + + function Auth() { + let childRoutes = createRoutesFromElements( + <> + Auth Login} /> + Not Found} /> + + ); + + return useRoutes(childRoutes); + } + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +

+ Not Found +

+ `); + + let renderer2: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer2 = TestRenderer.create( + + + } /> + + + ); + }); + + expect(renderer2.toJSON()).toMatchInlineSnapshot(` +

+ Auth Login +

+ `); + }); + + it("useRoutes() treats descendant route leading-slash paths as absolute when specified", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + } /> + + + ); + }); + + function Auth() { + let childRoutes = createRoutesFromElements( + <> + Auth Login} /> + Not Found} /> + + ); + return useRoutes(childRoutes, null, true); + } + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +

+ Auth Login +

+ `); + }); + }); + describe("works with paths beginning with special characters", () => { function PrintParams() { - return

The params are {JSON.stringify(useParams())}

; + return

The params are{JSON.stringify(useParams())}

; } function ReactCourses() { return ( @@ -124,7 +284,7 @@ describe("Descendant splat matching", () => { React Fundamentals

- The params are + The params are {"*":"-react-fundamentals","splat":"-react-fundamentals"}

@@ -132,6 +292,7 @@ describe("Descendant splat matching", () => { `); }); + it("allows `.` to appear at the beginning", () => { let renderer = renderNestedSplatRoute([ "/courses/react/.react-fundamentals", @@ -150,7 +311,7 @@ describe("Descendant splat matching", () => { React Fundamentals

- The params are + The params are {"*":".react-fundamentals","splat":".react-fundamentals"}

@@ -158,6 +319,7 @@ describe("Descendant splat matching", () => { `); }); + it("allows `~` to appear at the beginning", () => { let renderer = renderNestedSplatRoute([ "/courses/react/~react-fundamentals", @@ -176,7 +338,7 @@ describe("Descendant splat matching", () => { React Fundamentals

- The params are + The params are {"*":"~react-fundamentals","splat":"~react-fundamentals"}

@@ -184,6 +346,7 @@ describe("Descendant splat matching", () => { `); }); + it("allows `@` to appear at the beginning", () => { let renderer = renderNestedSplatRoute([ "/courses/react/@react-fundamentals", @@ -202,7 +365,7 @@ describe("Descendant splat matching", () => { React Fundamentals

- The params are + The params are {"*":"@react-fundamentals","splat":"@react-fundamentals"}

@@ -210,6 +373,7 @@ describe("Descendant splat matching", () => { `); }); + it("allows url-encoded entities to appear at the beginning", () => { let renderer = renderNestedSplatRoute([ "/courses/react/%20react-fundamentals", @@ -228,7 +392,7 @@ describe("Descendant splat matching", () => { React Fundamentals

- The params are + The params are {"*":" react-fundamentals","splat":" react-fundamentals"}

diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 918bc34d39..52f73ddf34 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -492,6 +492,7 @@ export function Router({ export interface RoutesProps { children?: React.ReactNode; location?: Partial | string; + absolute?: boolean; } /** @@ -503,8 +504,9 @@ export interface RoutesProps { export function Routes({ children, location, + absolute, }: RoutesProps): React.ReactElement | null { - return useRoutes(createRoutesFromChildren(children), location); + return useRoutes(createRoutesFromChildren(children), location, absolute); } export interface AwaitResolveRenderFunction { diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 5d705553e7..fbffb59fed 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -338,9 +338,10 @@ export function useResolvedPath( */ export function useRoutes( routes: RouteObject[], - locationArg?: Partial | string + locationArg?: Partial | string, + absolute?: boolean ): React.ReactElement | null { - return useRoutesImpl(routes, locationArg); + return useRoutesImpl(routes, locationArg, undefined, undefined, absolute); } // Internal implementation with accept optional param for RouterProvider usage @@ -348,7 +349,8 @@ export function useRoutesImpl( routes: RouteObject[], locationArg?: Partial | string, dataRouterState?: RemixRouter["state"], - future?: RemixRouter["future"] + future?: RemixRouter["future"], + absolute?: boolean ): React.ReactElement | null { invariant( useInRouterContext(), @@ -423,7 +425,7 @@ export function useRoutesImpl( let pathname = location.pathname || "/"; let remainingPathname = - parentPathnameBase === "/" + parentPathnameBase === "/" || absolute ? pathname : pathname.slice(parentPathnameBase.length) || "/"; From d0dcca70ee99641765228b8d94505a322ccd6adf Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 26 Jan 2024 10:04:31 -0500 Subject: [PATCH 2/6] Switch to AbsoluteRoutes/useAbsoluteRoutes --- docs/components/absolute-routes.md | 55 ++++++ docs/hooks/use-absolute-routes.md | 64 +++++++ .../__tests__/absolute-rotues-test.tsx | 130 ++++++++++++++ .../descendant-routes-splat-matching-test.tsx | 161 +----------------- packages/react-router/index.ts | 6 + packages/react-router/lib/components.tsx | 37 +++- packages/react-router/lib/hooks.tsx | 37 +++- 7 files changed, 324 insertions(+), 166 deletions(-) create mode 100644 docs/components/absolute-routes.md create mode 100644 docs/hooks/use-absolute-routes.md create mode 100644 packages/react-router/__tests__/absolute-rotues-test.tsx diff --git a/docs/components/absolute-routes.md b/docs/components/absolute-routes.md new file mode 100644 index 0000000000..5f477db979 --- /dev/null +++ b/docs/components/absolute-routes.md @@ -0,0 +1,55 @@ +--- +title: AbsoluteRoutes +--- + +# `` + +Rendered anywhere in the app, `` will match a set of child routes using absolute paths against the current [location][location] pathname. + +```tsx +interface AbsoluteRoutesProps { + children?: React.ReactNode; + location?: Partial | string; +} +``` + +If you're using a data router like [`createBrowserRouter`][createbrowserrouter] it is uncommon to use this component as routes defined as part of a descendant `` tree cannot leverage the [Data APIs][data-apis] available to [`RouterProvider`][router-provider] apps. You **can and should** use this component within your `RouterProvider` application [while you are migrating][migrating-to-router-provider]. + +This component is strictly a utility to be used to assist in migration from v5 to v6 so that folks can use absolute paths in descendant route definitions (which was a common pattern in RR v5). The intent is to remove this component in v7 so it is marked "deprecated" from the start as a reminder to work on moving your route definitions upwards out of descendant routes.

We expect the concept of "descendant routes" to be replaced by [Lazy Route Discovery][lazy-route-discovery-rfc] when that feature lands, so the plan is that folks can use `` to migrate from v5 to v6. Then, incrementally migrate those descendant routes to lazily discovered route `children` while on v6. Then when an eventual v7 releases, there will be no need for `AbsoluteRoutes` and it can be safely removed.
+ +Whenever the location changes, `` looks through all its child routes to find the best absolute-path match and renders that branch of the UI. `` elements may be nested to indicate nested UI, but their paths should all be specified via absolute paths. Parent routes render their child routes by rendering an [``][outlet]. + +```tsx +function App() { + return ( + + Home} /> + } /> + + ); +} + +function Auth() { + return ( + + }> + } /> + } /> + + + ); +} +``` + +See also: + +- [``][routes] + +[location]: ../utils/location +[outlet]: ./outlet +[createbrowserrouter]: ../routers/create-browser-router +[data-apis]: ../routers/picking-a-router#data-apis +[router-provider]: ../routers/router-provider +[migrating-to-router-provider]: ../upgrading/v6-data +[lazy-route-discovery-rfc]: https://github.com/remix-run/react-router/discussions/11113 +[routes]: ./routes diff --git a/docs/hooks/use-absolute-routes.md b/docs/hooks/use-absolute-routes.md new file mode 100644 index 0000000000..b430391706 --- /dev/null +++ b/docs/hooks/use-absolute-routes.md @@ -0,0 +1,64 @@ +--- +title: useAbsoluteRoutes +--- + +# `useAbsoluteRoutes` + +
+ Type declaration + +```tsx +declare function useAbsoluteRoutes( + routes: RouteObject[], + location?: Partial | string; +): React.ReactElement | null; +``` + +
+ +The `useAbsoluteRoutes` hook is the functional equivalent of [``][absoluteroutes], but it uses JavaScript objects instead of `` elements to define your routes. These objects have the same properties as normal [`` elements][route], but they don't require JSX. + +All route paths passed to `useAbsoluteRoutes` should be defined using absolute paths. + +This component is strictly a utility to be used to assist in migration from v5 to v6 so that folks can use absolute paths in descendant route definitions (which was a common pattern in RR v5). The intent is to remove this component in v7 so it is marked "deprecated" from the start as a reminder to work on moving your route definitions upwards out of descendant routes.

We expect the concept of "descendant routes" to be replaced by [Lazy Route Discovery][lazy-route-discovery-rfc] when that feature lands, so the plan is that folks can use `` to migrate from v5 to v6. Then, incrementally migrate those descendant routes to lazily discovered route `children` while on v6. Then when an eventual v7 releases, there will be no need for `AbsoluteRoutes` and it can be safely removed.
+ +The return value of `useAbsoluteRoutes` is either a valid React element you can use to render the route tree, or `null` if nothing matched. + +```tsx +import * as React from "react"; +import { useAbsoluteRoutes } from "react-router-dom"; + +function App() { + return ( + + Home} /> + } /> + + ); +} + +function Auth() { + let element = useAbsoluteRoutes([ + path: "/auth", + element: , + children: [{ + path: "/auth", + element: AuthHome, + }, { + path: "/auth/login", + element: AuthLogin, + }], + }]); + + return element; +} +``` + +See also: + +- [`useRoutes`][useroutes] + +[absoluteroutes]: ../components/absolute-routes +[route]: ../components/route +[lazy-route-discovery-rfc]: https://github.com/remix-run/react-router/discussions/11113 +[useroutes]: ./use-routes diff --git a/packages/react-router/__tests__/absolute-rotues-test.tsx b/packages/react-router/__tests__/absolute-rotues-test.tsx new file mode 100644 index 0000000000..1ff57d9e5e --- /dev/null +++ b/packages/react-router/__tests__/absolute-rotues-test.tsx @@ -0,0 +1,130 @@ +import * as React from "react"; +import * as TestRenderer from "react-test-renderer"; +import { + AbsoluteRoutes, + MemoryRouter, + Routes, + Route, + createRoutesFromElements, + useRoutes, + useAbsoluteRoutes, +} from "react-router"; + +describe("/useAbsoluteRoutes", () => { + it(" treats descendant route paths as absolute", () => { + function App({ url }) { + return ( + + + } /> + + + ); + } + + function Auth() { + return ( + + Auth Login} /> + Nope} /> + Not Found} /> + + ); + } + + // Matches absolute descendant routes + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create(); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +

+ Auth Login +

+ `); + + // Falls through to splat/not-found routes + let renderer2: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer2 = TestRenderer.create(); + }); + + expect(renderer2.toJSON()).toMatchInlineSnapshot(` +

+ Not Found +

+ `); + + // Does not match child relative paths + let renderer3: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer3 = TestRenderer.create(); + }); + + expect(renderer3.toJSON()).toMatchInlineSnapshot(` +

+ Not Found +

+ `); + }); + + it("useAbsoluteRoutes() treats descendant route paths as absolute", () => { + function App({ url }) { + return ( + + + } /> + + + ); + } + + function Auth() { + let childRoutes = createRoutesFromElements( + <> + Auth Login} /> + Nope} /> + Not Found} /> + + ); + return useAbsoluteRoutes(childRoutes); + } + + // Matches absolute descendant routes + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create(); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +

+ Auth Login +

+ `); + + // Falls through to splat/not-found routes + let renderer2: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer2 = TestRenderer.create(); + }); + + expect(renderer2.toJSON()).toMatchInlineSnapshot(` +

+ Not Found +

+ `); + + // Does not match child relative paths + let renderer3: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer3 = TestRenderer.create(); + }); + + expect(renderer3.toJSON()).toMatchInlineSnapshot(` +

+ Not Found +

+ `); + }); +}); diff --git a/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx b/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx index ae6554ccd4..b9c05b7c21 100644 --- a/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx +++ b/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx @@ -1,14 +1,6 @@ import * as React from "react"; import * as TestRenderer from "react-test-renderer"; -import { - MemoryRouter, - Outlet, - Routes, - Route, - useParams, - createRoutesFromElements, - useRoutes, -} from "react-router"; +import { MemoryRouter, Outlet, Routes, Route, useParams } from "react-router"; import type { InitialEntry } from "@remix-run/router"; describe("Descendant splat matching", () => { @@ -67,157 +59,6 @@ describe("Descendant splat matching", () => { `); }); - describe("/useRoutes absolute config", () => { - it(" treats descendant route leading-slash paths as relative by default", () => { - let renderer: TestRenderer.ReactTestRenderer; - TestRenderer.act(() => { - renderer = TestRenderer.create( - - - } /> - - - ); - }); - - function Auth() { - return ( - - Auth Login} /> - Not Found} /> - - ); - } - - expect(renderer.toJSON()).toMatchInlineSnapshot(` -

- Not Found -

- `); - - let renderer2: TestRenderer.ReactTestRenderer; - TestRenderer.act(() => { - renderer2 = TestRenderer.create( - - - } /> - - - ); - }); - - expect(renderer2.toJSON()).toMatchInlineSnapshot(` -

- Auth Login -

- `); - }); - - it(" treats descendant route leading-slash paths as absolute when specified", () => { - let renderer: TestRenderer.ReactTestRenderer; - TestRenderer.act(() => { - renderer = TestRenderer.create( - - - } /> - - - ); - }); - - function Auth() { - return ( - - Auth Login} /> - Not Found} /> - - ); - } - - expect(renderer.toJSON()).toMatchInlineSnapshot(` -

- Auth Login -

- `); - }); - - it("useRoutes() treats descendant route leading-slash paths as relative by default", () => { - let renderer: TestRenderer.ReactTestRenderer; - TestRenderer.act(() => { - renderer = TestRenderer.create( - - - } /> - - - ); - }); - - function Auth() { - let childRoutes = createRoutesFromElements( - <> - Auth Login} /> - Not Found} /> - - ); - - return useRoutes(childRoutes); - } - - expect(renderer.toJSON()).toMatchInlineSnapshot(` -

- Not Found -

- `); - - let renderer2: TestRenderer.ReactTestRenderer; - TestRenderer.act(() => { - renderer2 = TestRenderer.create( - - - } /> - - - ); - }); - - expect(renderer2.toJSON()).toMatchInlineSnapshot(` -

- Auth Login -

- `); - }); - - it("useRoutes() treats descendant route leading-slash paths as absolute when specified", () => { - let renderer: TestRenderer.ReactTestRenderer; - TestRenderer.act(() => { - renderer = TestRenderer.create( - - - } /> - - - ); - }); - - function Auth() { - let childRoutes = createRoutesFromElements( - <> - Auth Login} /> - Not Found} /> - - ); - return useRoutes(childRoutes, null, true); - } - - expect(renderer.toJSON()).toMatchInlineSnapshot(` -

- Auth Login -

- `); - }); - }); - describe("works with paths beginning with special characters", () => { function PrintParams() { return

The params are{JSON.stringify(useParams())}

; diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index fb7b787797..b9afb0867a 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -49,6 +49,7 @@ import { } from "@remix-run/router"; import type { + AbsoluteRoutesProps, AwaitProps, FutureConfig, IndexRouteProps, @@ -63,6 +64,7 @@ import type { RoutesProps, } from "./lib/components"; import { + AbsoluteRoutes, Await, MemoryRouter, Navigate, @@ -93,6 +95,7 @@ import { } from "./lib/context"; import type { NavigateFunction } from "./lib/hooks"; import { + useAbsoluteRoutes, useActionData, useAsyncError, useAsyncValue, @@ -125,6 +128,7 @@ type Search = string; // Expose react-router public API export type { + AbsoluteRoutesProps, ActionFunction, ActionFunctionArgs, AwaitProps, @@ -176,6 +180,7 @@ export type { }; export { AbortedDeferredError, + AbsoluteRoutes, Await, MemoryRouter, Navigate, @@ -200,6 +205,7 @@ export { renderMatches, resolvePath, useBlocker, + useAbsoluteRoutes, useActionData, useAsyncError, useAsyncValue, diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 52f73ddf34..1415746ea3 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -41,6 +41,7 @@ import { } from "./context"; import { _renderMatches, + useAbsoluteRoutes, useAsyncValue, useInRouterContext, useLocation, @@ -492,7 +493,6 @@ export function Router({ export interface RoutesProps { children?: React.ReactNode; location?: Partial | string; - absolute?: boolean; } /** @@ -504,9 +504,40 @@ export interface RoutesProps { export function Routes({ children, location, - absolute, }: RoutesProps): React.ReactElement | null { - return useRoutes(createRoutesFromChildren(children), location, absolute); + return useRoutes(createRoutesFromChildren(children), location); +} + +export interface AbsoluteRoutesProps extends RoutesProps {} + +/** + * @deprecated + * A container for a nested tree of `` elements that renders the branch + * that best matches the current location using absolute path matching. + * + * IMPORTANT: This is strictly a utility to be used to assist in migration + * from v5 to v6 so that folks can use absolute paths in descendant route + * definitions (which was a common pattern in RR v5). The intent is to remove + * this component in v7 so it is marked "deprecated" from the start as a reminder + * to work on moving your route definitions upwards out of descendant routes. + * + * We expect the concept of "descendant routes" to be replaced by "Lazy Route + * Discovery" when that feature lands, so the plan is that folks can use + * `` to migrate from v5->v6. Then, incrementally migrate those + * descendant routes to lazily discovered route `children` while on v6. Then + * when an eventual v7 releases, there will be no need for AbsoluteRoutes and + * it can be safely removed. + * + * See the RFC for Lazy Route Discovery in: + * https://github.com/remix-run/react-router/discussions/11113) + * + * @see https://reactrouter.com/components/absolute-routes + */ +export function AbsoluteRoutes({ + children, + location, +}: AbsoluteRoutesProps): React.ReactElement | null { + return useAbsoluteRoutes(createRoutesFromChildren(children), location); } export interface AwaitResolveRenderFunction { diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index fbffb59fed..50fb4f7588 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -338,10 +338,41 @@ export function useResolvedPath( */ export function useRoutes( routes: RouteObject[], - locationArg?: Partial | string, - absolute?: boolean + locationArg?: Partial | string +): React.ReactElement | null { + return useRoutesImpl(routes, locationArg); +} + +/** + * @deprecated + * Returns the element of the route that matched the current location using + * absolute path matching, prepared with the correct context to render the + * remainder of the route tree. Route elements in the tree must render an + * `` to render their child route's element. + * + * IMPORTANT: This is strictly a utility to be used to assist in migration + * from v5 to v6 so that folks can use absolute paths in descendant route + * definitions (which was a common pattern in RR v5). The intent is to remove + * this hook in v7 so it is marked "deprecated" from the start as a reminder + * to work on moving your route definitions upwards out of descendant routes. + * + * We expect the concept of "descendant routes" to be replaced by "Lazy Route + * Discovery" when that feature lands, so the plan is that folks can use + * `useAbsoluteRoutes` to migrate from v5->v6. Then, incrementally migrate those + * descendant routes to lazily discovered route `children` while on v6. Then + * when an eventual v7 releases, there will be no need for `useAbsoluteRoutes` + * and it can be safely removed. + * + * See the RFC for Lazy Route Discovery in: + * https://github.com/remix-run/react-router/discussions/11113) + * + * @see https://reactrouter.com/hooks/use-absolute-routes + */ +export function useAbsoluteRoutes( + routes: RouteObject[], + locationArg?: Partial | string ): React.ReactElement | null { - return useRoutesImpl(routes, locationArg, undefined, undefined, absolute); + return useRoutesImpl(routes, locationArg, undefined, undefined, true); } // Internal implementation with accept optional param for RouterProvider usage From 5c8a4e31f3c76711cc8964842b3ca8ab4e67b117 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 26 Jan 2024 10:05:20 -0500 Subject: [PATCH 3/6] Revert changes to unrelated test suite --- .../descendant-routes-splat-matching-test.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx b/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx index b9c05b7c21..a5fce5a781 100644 --- a/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx +++ b/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx @@ -58,10 +58,9 @@ describe("Descendant splat matching", () => { `); }); - describe("works with paths beginning with special characters", () => { function PrintParams() { - return

The params are{JSON.stringify(useParams())}

; + return

The params are {JSON.stringify(useParams())}

; } function ReactCourses() { return ( @@ -125,7 +124,7 @@ describe("Descendant splat matching", () => { React Fundamentals

- The params are + The params are {"*":"-react-fundamentals","splat":"-react-fundamentals"}

@@ -133,7 +132,6 @@ describe("Descendant splat matching", () => { `); }); - it("allows `.` to appear at the beginning", () => { let renderer = renderNestedSplatRoute([ "/courses/react/.react-fundamentals", @@ -152,7 +150,7 @@ describe("Descendant splat matching", () => { React Fundamentals

- The params are + The params are {"*":".react-fundamentals","splat":".react-fundamentals"}

@@ -160,7 +158,6 @@ describe("Descendant splat matching", () => { `); }); - it("allows `~` to appear at the beginning", () => { let renderer = renderNestedSplatRoute([ "/courses/react/~react-fundamentals", @@ -179,7 +176,7 @@ describe("Descendant splat matching", () => { React Fundamentals

- The params are + The params are {"*":"~react-fundamentals","splat":"~react-fundamentals"}

@@ -187,7 +184,6 @@ describe("Descendant splat matching", () => { `); }); - it("allows `@` to appear at the beginning", () => { let renderer = renderNestedSplatRoute([ "/courses/react/@react-fundamentals", @@ -206,7 +202,7 @@ describe("Descendant splat matching", () => { React Fundamentals

- The params are + The params are {"*":"@react-fundamentals","splat":"@react-fundamentals"}

@@ -214,7 +210,6 @@ describe("Descendant splat matching", () => { `); }); - it("allows url-encoded entities to appear at the beginning", () => { let renderer = renderNestedSplatRoute([ "/courses/react/%20react-fundamentals", @@ -233,7 +228,7 @@ describe("Descendant splat matching", () => { React Fundamentals

- The params are + The params are {"*":" react-fundamentals","splat":" react-fundamentals"}

From 08b50e11a078dd84f441569a83ee272960afd684 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 26 Jan 2024 10:06:17 -0500 Subject: [PATCH 4/6] Fix misnamed test file --- .../{absolute-rotues-test.tsx => absolute-routes-test.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/react-router/__tests__/{absolute-rotues-test.tsx => absolute-routes-test.tsx} (100%) diff --git a/packages/react-router/__tests__/absolute-rotues-test.tsx b/packages/react-router/__tests__/absolute-routes-test.tsx similarity index 100% rename from packages/react-router/__tests__/absolute-rotues-test.tsx rename to packages/react-router/__tests__/absolute-routes-test.tsx From cf07c6ad9494a8dd6092a074dce84a3b162751e4 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 26 Jan 2024 13:09:42 -0500 Subject: [PATCH 5/6] Re-export through other packages --- packages/react-router-dom-v5-compat/index.ts | 3 +++ packages/react-router-dom/index.tsx | 3 +++ packages/react-router-native/index.tsx | 3 +++ 3 files changed, 9 insertions(+) diff --git a/packages/react-router-dom-v5-compat/index.ts b/packages/react-router-dom-v5-compat/index.ts index ae07125d13..191a100e60 100644 --- a/packages/react-router-dom-v5-compat/index.ts +++ b/packages/react-router-dom-v5-compat/index.ts @@ -47,6 +47,7 @@ * deprecate the deep require if we wanted to avoid the duplication here. */ export type { + AbsoluteRoutesProps, ActionFunction, ActionFunctionArgs, AwaitProps, @@ -112,6 +113,7 @@ export type { } from "./react-router-dom"; export { AbortedDeferredError, + AbsoluteRoutes, Await, BrowserRouter, Form, @@ -155,6 +157,7 @@ export { unstable_HistoryRouter, useBlocker, unstable_usePrompt, + useAbsoluteRoutes, useActionData, useAsyncError, useAsyncValue, diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 532658706e..a94da58edb 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -95,6 +95,7 @@ export { createSearchParams }; // Note: Keep in sync with react-router exports! export type { + AbsoluteRoutesProps, ActionFunction, ActionFunctionArgs, AwaitProps, @@ -146,6 +147,7 @@ export type { } from "react-router"; export { AbortedDeferredError, + AbsoluteRoutes, Await, MemoryRouter, Navigate, @@ -169,6 +171,7 @@ export { redirectDocument, renderMatches, resolvePath, + useAbsoluteRoutes, useActionData, useAsyncError, useAsyncValue, diff --git a/packages/react-router-native/index.tsx b/packages/react-router-native/index.tsx index f88de93b3d..0bc8369add 100644 --- a/packages/react-router-native/index.tsx +++ b/packages/react-router-native/index.tsx @@ -20,6 +20,7 @@ import URLSearchParams from "@ungap/url-search-params"; // Note: Keep in sync with react-router exports! export type { + AbsoluteRoutesProps, ActionFunction, ActionFunctionArgs, AwaitProps, @@ -71,6 +72,7 @@ export type { } from "react-router"; export { AbortedDeferredError, + AbsoluteRoutes, Await, MemoryRouter, Navigate, @@ -95,6 +97,7 @@ export { redirectDocument, renderMatches, resolvePath, + useAbsoluteRoutes, useActionData, useAsyncError, useAsyncValue, From b1d337cf85d798a30e6735d0ed304f54a86c5e2e Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 26 Jan 2024 13:21:29 -0500 Subject: [PATCH 6/6] Add more tests --- .../__tests__/absolute-routes-test.tsx | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/packages/react-router/__tests__/absolute-routes-test.tsx b/packages/react-router/__tests__/absolute-routes-test.tsx index 1ff57d9e5e..8632c2e6ab 100644 --- a/packages/react-router/__tests__/absolute-routes-test.tsx +++ b/packages/react-router/__tests__/absolute-routes-test.tsx @@ -8,6 +8,7 @@ import { createRoutesFromElements, useRoutes, useAbsoluteRoutes, + Outlet, } from "react-router"; describe("/useAbsoluteRoutes", () => { @@ -127,4 +128,174 @@ describe("/useAbsoluteRoutes", () => { `); }); + + it("works for descendant pathless layout routes (no path specified)", () => { + function App({ url }) { + return ( + + + } /> + + + ); + } + + function Auth() { + return ( + + }> + Auth Login} /> + Nope} /> + Not Found} /> + + + ); + } + + function AuthLayout() { + return ( + <> +

Auth Layout

+ + + ); + } + + // Matches absolute descendant routes + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create(); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` + [ +

+ Auth Layout +

, +

+ Auth Login +

, + ] + `); + + // Falls through to splat/not-found routes + let renderer2: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer2 = TestRenderer.create(); + }); + + expect(renderer2.toJSON()).toMatchInlineSnapshot(` + [ +

+ Auth Layout +

, +

+ Not Found +

, + ] + `); + + // Does not match child relative paths + let renderer3: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer3 = TestRenderer.create(); + }); + + expect(renderer3.toJSON()).toMatchInlineSnapshot(` + [ +

+ Auth Layout +

, +

+ Not Found +

, + ] + `); + }); + + it("works for descendant pathless layout routes (absolute path)", () => { + // Once you do an absolute layout route, you can start using relative on + // children again since they get flattened down together during matching + function App({ url }) { + return ( + + + } /> + + + ); + } + + function Auth() { + return ( + + }> + Auth Login} /> + Works} /> + Not Found} /> + + + ); + } + + function AuthLayout() { + return ( + <> +

Auth Layout

+ + + ); + } + + // Matches absolute descendant routes + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create(); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` + [ +

+ Auth Layout +

, +

+ Auth Login +

, + ] + `); + + // Falls through to splat/not-found routes + let renderer2: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer2 = TestRenderer.create(); + }); + + expect(renderer2.toJSON()).toMatchInlineSnapshot(` + [ +

+ Auth Layout +

, +

+ Not Found +

, + ] + `); + + // Does not match child relative paths + let renderer3: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer3 = TestRenderer.create(); + }); + + expect(renderer3.toJSON()).toMatchInlineSnapshot(` + [ +

+ Auth Layout +

, +

+ Works +

, + ] + `); + }); });