From 42aa82cdadb587acc70baa5b507873237b94a1cd Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 29 May 2025 13:27:17 -0400 Subject: [PATCH 1/6] Add support for AbsoluteRoutes/useAbsoluteRoutes --- .changeset/silly-worms-sleep.md | 5 + docs/api/components/AbsoluteRoutes.md | 47 +++ docs/api/hooks/useAbsoluteRoutes.md | 78 +++++ .../__tests__/absolute-routes-test.tsx | 300 ++++++++++++++++++ packages/react-router/index.ts | 3 + packages/react-router/lib/components.tsx | 34 ++ packages/react-router/lib/hooks.tsx | 18 +- 7 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 .changeset/silly-worms-sleep.md create mode 100644 docs/api/components/AbsoluteRoutes.md create mode 100644 docs/api/hooks/useAbsoluteRoutes.md create mode 100644 packages/react-router/__tests__/absolute-routes-test.tsx diff --git a/.changeset/silly-worms-sleep.md b/.changeset/silly-worms-sleep.md new file mode 100644 index 0000000000..9485d21b1e --- /dev/null +++ b/.changeset/silly-worms-sleep.md @@ -0,0 +1,5 @@ +--- +"react-router": minor +--- + +Implement ``/`useAbsoluteRoutes` as alternatiuves to ``/`useRoutes` when using absolute paths is required. This is primarily intended to be used to ease migrations from v5 applications where this was a common pattern. It's also useful for descendant routes when you want to manage your paths in an external data structure. diff --git a/docs/api/components/AbsoluteRoutes.md b/docs/api/components/AbsoluteRoutes.md new file mode 100644 index 0000000000..a07885e604 --- /dev/null +++ b/docs/api/components/AbsoluteRoutes.md @@ -0,0 +1,47 @@ +--- +title: AbsoluteRoutes +--- + +# AbsoluteRoutes + +[MODES: framework, data, declarative] + +## Summary + +[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.AbsoluteRoutes.html) + +An alternate version of [](./Routes) that expects absolute paths on routes instead of relative paths. This is mostly intended to be used as a tool to help migrate from v5 where absolute paths were a common pattern, or for when you want to define your paths in a separate data structure using absolute paths. + +```tsx +import { AbsoluteRoutes, Route } from "react-router"; + + + } /> +; + +function Dashboard() { + return ( + + } + /> + } /> + + ); +} +``` + +## Props + +### children + +[modes: framework, data, declarative] + +Nested [Route](../components/Route) elements + +### location + +[modes: framework, data, declarative] + +The location to match against. Defaults to the current location. diff --git a/docs/api/hooks/useAbsoluteRoutes.md b/docs/api/hooks/useAbsoluteRoutes.md new file mode 100644 index 0000000000..c6c9f23fb7 --- /dev/null +++ b/docs/api/hooks/useAbsoluteRoutes.md @@ -0,0 +1,78 @@ +--- +title: useAbsoluteRoutes +--- + +# useAbsoluteRoutes + +[MODES: framework, data, declarative] + +## Summary + +[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useAbsoluteRoutes.html) + +An alternate version of [useRoutes](./useRoutes) that expects absolute paths on routes instead of relative paths. This is mostly intended to be used as a tool to help migrate from v5 where absolute paths were a common pattern, or for when you want to define your paths in a separate data structure using absolute paths. + +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"; + +const routes = { + dashboard: { + path: "/dashboard", + href: () => `/dashboard`, + }, + dashboardMessages: { + path: "/dashboard/messages", + href: () => `/dashboard/messages`, + }, + dashboardMessage: { + path: "/dashboard/:id", + href: (id: number) => `/dashboard/${id}`, + }, +}; + +function App() { + let element = useAbsoluteRoutes([ + { + path: routes.dashboard.path, + element: , + children: [ + { + path: routes.dashboardMessages.path, + element: , + children: [ + { + path: routes.dashboardMessage.path, + element: , + }, + ], + }, + ], + }, + ]); + + return element; +} +``` + +## Signature + +```tsx +useAbsoluteRoutes(routes, locationArg): undefined +``` + +## Params + +### routes + +[modes: framework, data, declarative] + +Your routes to use to render this location + +### locationArg + +[modes: framework, data, declarative] + +The location to render instead of the current location diff --git a/packages/react-router/__tests__/absolute-routes-test.tsx b/packages/react-router/__tests__/absolute-routes-test.tsx new file mode 100644 index 0000000000..19d618373d --- /dev/null +++ b/packages/react-router/__tests__/absolute-routes-test.tsx @@ -0,0 +1,300 @@ +import * as React from "react"; +import * as TestRenderer from "react-test-renderer"; +import { + AbsoluteRoutes, + MemoryRouter, + Routes, + Route, + createRoutesFromElements, + useAbsoluteRoutes, + Outlet, +} 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 +

+ `); + }); + + 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 +

, + ] + `); + }); +}); diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index fddcd940eb..a5e4158e9c 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -89,6 +89,7 @@ export type { RouteObject, } from "./lib/context"; export type { + AbsoluteRoutesProps, AwaitProps, IndexRouteProps, LayoutRouteProps, @@ -103,6 +104,7 @@ export type { RoutesProps, } from "./lib/components"; export { + AbsoluteRoutes, Await, MemoryRouter, Navigate, @@ -118,6 +120,7 @@ export { } from "./lib/components"; export type { NavigateFunction } from "./lib/hooks"; export { + useAbsoluteRoutes, useBlocker, useActionData, useAsyncError, diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 769dc28bf1..aceb51d856 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -53,6 +53,7 @@ import { } from "./context"; import { _renderMatches, + useAbsoluteRoutes, useActionData, useAsyncValue, useInRouterContext, @@ -865,6 +866,39 @@ export function Routes({ return useRoutes(createRoutesFromChildren(children), location); } +export interface AbsoluteRoutesProps extends RoutesProps {} + +/** + * An alternate implementation of `` that expects absolute paths even + * when used as descendant routes. Note that these routes do not participate in + * data loading, actions, code splitting, or any other route module features. + * + * ```tsx + * import { AbsoluteRoutes, Route } from "react-router" + * + * + * } /> + * + * + * function Dashboard() { + * return ( + * + * } /> + * } /> + * + * ); + * } + * ``` + * + * @category Components + */ +export function AbsoluteRoutes({ + children, + location, +}: AbsoluteRoutesProps): React.ReactElement | null { + return useAbsoluteRoutes(createRoutesFromChildren(children), location); +} + export interface AwaitResolveRenderFunction { (data: Awaited): React.ReactNode; } diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 2b0953d73b..9a9a374e5f 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -425,6 +425,19 @@ export function useRoutes( return useRoutesImpl(routes, locationArg); } +/** + * 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. + */ +export function useAbsoluteRoutes( + routes: RouteObject[], + locationArg?: Partial | string +): React.ReactElement | null { + return useRoutesImpl(routes, locationArg, undefined, undefined, true); +} + /** * Internal implementation with accept optional param for RouterProvider usage * @@ -435,7 +448,8 @@ export function useRoutesImpl( routes: RouteObject[], locationArg?: Partial | string, dataRouterState?: DataRouter["state"], - future?: DataRouter["future"] + future?: DataRouter["future"], + absolute?: boolean ): React.ReactElement | null { invariant( useInRouterContext(), @@ -511,7 +525,7 @@ export function useRoutesImpl( let pathname = location.pathname || "/"; let remainingPathname = pathname; - if (parentPathnameBase !== "/") { + if (!absolute && parentPathnameBase !== "/") { // Determine the remaining pathname by removing the # of URL segments the // parentPathnameBase has, instead of removing based on character count. // This is because we can't guarantee that incoming/outgoing encodings/ From b9c05f0ebcc7b3474931661412f9a29b3050ac28 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 24 Jun 2025 10:53:32 -0400 Subject: [PATCH 2/6] Update docs --- docs/api/components/AbsoluteRoutes.md | 2 +- docs/api/hooks/useAbsoluteRoutes.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/components/AbsoluteRoutes.md b/docs/api/components/AbsoluteRoutes.md index a07885e604..fdc81f8063 100644 --- a/docs/api/components/AbsoluteRoutes.md +++ b/docs/api/components/AbsoluteRoutes.md @@ -38,7 +38,7 @@ function Dashboard() { [modes: framework, data, declarative] -Nested [Route](../components/Route) elements +Nested [Route](../components/Route) elements using absolute paths ### location diff --git a/docs/api/hooks/useAbsoluteRoutes.md b/docs/api/hooks/useAbsoluteRoutes.md index c6c9f23fb7..ef1600204d 100644 --- a/docs/api/hooks/useAbsoluteRoutes.md +++ b/docs/api/hooks/useAbsoluteRoutes.md @@ -10,7 +10,7 @@ title: useAbsoluteRoutes [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useAbsoluteRoutes.html) -An alternate version of [useRoutes](./useRoutes) that expects absolute paths on routes instead of relative paths. This is mostly intended to be used as a tool to help migrate from v5 where absolute paths were a common pattern, or for when you want to define your paths in a separate data structure using absolute paths. +An alternate version of [useRoutes](./useRoutes) that expects absolute paths on routes instead of relative paths. This is mostly intended to be used as a tool to help migrate from v5 where absolute paths were a common pattern, or for when you want to define your paths in a separate data structure using absolute paths. This hook expects absolute paths both when used at the top level of your application, or within a set of descendant routes inside a splat route. The return value of `useAbsoluteRoutes` is either a valid React element you can use to render the route tree, or `null` if nothing matched. @@ -69,7 +69,7 @@ useAbsoluteRoutes(routes, locationArg): undefined [modes: framework, data, declarative] -Your routes to use to render this location +Your routes to use to render this location, defined using absolute paths ### locationArg From 7239a5deb32e53d6a826548110ba0850b11bf6d1 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 25 Jun 2025 16:10:24 -0400 Subject: [PATCH 3/6] Update to unstable to follow OG process --- docs/api/components/AbsoluteRoutes.md | 11 +++++++---- docs/api/hooks/useAbsoluteRoutes.md | 12 ++++++------ packages/react-router/index.ts | 6 +++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/api/components/AbsoluteRoutes.md b/docs/api/components/AbsoluteRoutes.md index fdc81f8063..9415468bb0 100644 --- a/docs/api/components/AbsoluteRoutes.md +++ b/docs/api/components/AbsoluteRoutes.md @@ -1,19 +1,22 @@ --- -title: AbsoluteRoutes +title: unstable_AbsoluteRoutes --- -# AbsoluteRoutes +# unstable_AbsoluteRoutes [MODES: framework, data, declarative] ## Summary -[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.AbsoluteRoutes.html) +[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.unstable_AbsoluteRoutes.html) An alternate version of [](./Routes) that expects absolute paths on routes instead of relative paths. This is mostly intended to be used as a tool to help migrate from v5 where absolute paths were a common pattern, or for when you want to define your paths in a separate data structure using absolute paths. ```tsx -import { AbsoluteRoutes, Route } from "react-router"; +import { + unstable_AbsoluteRoutes as AbsoluteRoutes, + Route, +} from "react-router"; } /> diff --git a/docs/api/hooks/useAbsoluteRoutes.md b/docs/api/hooks/useAbsoluteRoutes.md index ef1600204d..a39f91b415 100644 --- a/docs/api/hooks/useAbsoluteRoutes.md +++ b/docs/api/hooks/useAbsoluteRoutes.md @@ -1,22 +1,22 @@ --- -title: useAbsoluteRoutes +title: unstable_useAbsoluteRoutes --- -# useAbsoluteRoutes +# unstable_useAbsoluteRoutes [MODES: framework, data, declarative] ## Summary -[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useAbsoluteRoutes.html) +[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.unstable_useAbsoluteRoutes.html) An alternate version of [useRoutes](./useRoutes) that expects absolute paths on routes instead of relative paths. This is mostly intended to be used as a tool to help migrate from v5 where absolute paths were a common pattern, or for when you want to define your paths in a separate data structure using absolute paths. This hook expects absolute paths both when used at the top level of your application, or within a set of descendant routes inside a splat route. -The return value of `useAbsoluteRoutes` is either a valid React element you can use to render the route tree, or `null` if nothing matched. +The return value of `unstable_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"; +import { unstable_useAbsoluteRoutes as useAbsoluteRoutes } from "react-router"; const routes = { dashboard: { @@ -60,7 +60,7 @@ function App() { ## Signature ```tsx -useAbsoluteRoutes(routes, locationArg): undefined +unstable_useAbsoluteRoutes(routes, locationArg): undefined ``` ## Params diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index a5e4158e9c..6f6c4f4188 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -89,7 +89,7 @@ export type { RouteObject, } from "./lib/context"; export type { - AbsoluteRoutesProps, + AbsoluteRoutesProps as unstable_AbsoluteRoutesProps, AwaitProps, IndexRouteProps, LayoutRouteProps, @@ -104,7 +104,7 @@ export type { RoutesProps, } from "./lib/components"; export { - AbsoluteRoutes, + AbsoluteRoutes as unstable_AbsoluteRoutes, Await, MemoryRouter, Navigate, @@ -120,7 +120,7 @@ export { } from "./lib/components"; export type { NavigateFunction } from "./lib/hooks"; export { - useAbsoluteRoutes, + useAbsoluteRoutes as unstable_useAbsoluteRoutes, useBlocker, useActionData, useAsyncError, From f696b4d6964458a9595c26dba0997b11514fc845 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 25 Jun 2025 16:13:39 -0400 Subject: [PATCH 4/6] Update changeset --- .changeset/silly-worms-sleep.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/silly-worms-sleep.md b/.changeset/silly-worms-sleep.md index 9485d21b1e..9017ae1e66 100644 --- a/.changeset/silly-worms-sleep.md +++ b/.changeset/silly-worms-sleep.md @@ -1,5 +1,5 @@ --- -"react-router": minor +"react-router": patch --- -Implement ``/`useAbsoluteRoutes` as alternatiuves to ``/`useRoutes` when using absolute paths is required. This is primarily intended to be used to ease migrations from v5 applications where this was a common pattern. It's also useful for descendant routes when you want to manage your paths in an external data structure. +Implement ``/`unstable_useAbsoluteRoutes` as alternatiuves to ``/`useRoutes` when using absolute paths is required. This is primarily intended to be used to ease migrations from v5 applications where this was a common pattern. It's also useful for descendant routes when you want to manage your paths in an external data structure. From 1ea0577dae2e348153c4e4531af9e7d6ef7b8146 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 26 Jun 2025 10:20:16 -0400 Subject: [PATCH 5/6] Fix tests --- packages/react-router/__tests__/absolute-routes-test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-router/__tests__/absolute-routes-test.tsx b/packages/react-router/__tests__/absolute-routes-test.tsx index 19d618373d..80a4f6dc31 100644 --- a/packages/react-router/__tests__/absolute-routes-test.tsx +++ b/packages/react-router/__tests__/absolute-routes-test.tsx @@ -1,12 +1,12 @@ import * as React from "react"; import * as TestRenderer from "react-test-renderer"; import { - AbsoluteRoutes, + unstable_AbsoluteRoutes as AbsoluteRoutes, MemoryRouter, Routes, Route, createRoutesFromElements, - useAbsoluteRoutes, + unstable_useAbsoluteRoutes as useAbsoluteRoutes, Outlet, } from "react-router"; From b588f7e6471ab45183583243c9d02291df29e120 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 27 Jun 2025 12:02:51 -0400 Subject: [PATCH 6/6] Update jsdoc exmaple to use unstable api --- packages/react-router/lib/components.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index aceb51d856..2dccc81e65 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -874,7 +874,7 @@ export interface AbsoluteRoutesProps extends RoutesProps {} * data loading, actions, code splitting, or any other route module features. * * ```tsx - * import { AbsoluteRoutes, Route } from "react-router" + * import { unstable_AbsoluteRoutes as AbsoluteRoutes, Route } from "react-router" * * * } />