location.state: {JSON.stringify(useLocation().state)}
; + returnlocation.state:{JSON.stringify(useLocation().state)}
; } let renderer: TestRenderer.ReactTestRenderer; @@ -91,7 +91,7 @@ describe("useNavigate", () => { expect(renderer.toJSON()).toMatchInlineSnapshot(`- location.state: + location.state: {"from":"home"}
`); diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 02fc92a20..953776481 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -323,7 +323,9 @@ export function Router({ ` You should never have more than one in your app.` ); - let basename = normalizePathname(basenameProp); + // Preserve trailing slashes on basename, so we can let the user control + // the enforcement of trailing slashes throughout the app + let basename = basenameProp.replace(/^\/*/, "/"); let navigationContext = React.useMemo( () => ({ basename, navigator, static: staticProp }), [basename, navigator, staticProp] diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index e11e23051..5e4cb7fc2 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { isRouteErrorResponse, Location, + normalizePathname, ParamParseKey, Params, Path, @@ -54,11 +55,24 @@ export function useHref(to: To): string { let joinedPathname = pathname; if (basename !== "/") { let toPathname = getToPathname(to); - let endsWithSlash = toPathname != null && toPathname.endsWith("/"); - joinedPathname = - pathname === "/" - ? basename + (endsWithSlash ? "/" : "") - : joinPaths([basename, pathname]); + + // Only append a trailing slash to the raw basename if the basename doesn't + // already have one and this wasn't specifically a route to "". This + // allows folks to control the trailing slash behavior when using a basename + let appendSlash = + !basename.endsWith("/") && + to !== "" && + (to as Path)?.pathname !== "" && + toPathname != null && + toPathname.endsWith("/"); + + if (pathname !== "/") { + joinedPathname = joinPaths([basename, pathname]); + } else if (appendSlash) { + joinedPathname = basename + "/"; + } else { + joinedPathname = basename; + } } return navigator.createHref({ pathname: joinedPathname, search, hash }); @@ -194,7 +208,13 @@ export function useNavigate(): NavigateFunction { ); if (basename !== "/") { - path.pathname = joinPaths([basename, path.pathname]); + // If this was a blank path, just use the basename directly, this gives + // the user control over trailing slash behavior + let toPath = typeof to === "string" ? parsePath(to) : to; + let isBlankPath = toPath.pathname == null || toPath.pathname === ""; + path.pathname = isBlankPath + ? basename + : joinPaths([basename, path.pathname]); } (!!options.replace ? navigator.replace : navigator.push)( diff --git a/packages/router/utils.ts b/packages/router/utils.ts index dec414313..77ad209f5 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -578,13 +578,18 @@ export function stripBasename( return null; } - let nextChar = pathname.charAt(basename.length); + // We want to leave trailing slash behavior in the user's control, so if they + // specify a basename with a trailing slash, we should support it + let startIndex = basename.endsWith("/") + ? basename.length - 1 + : basename.length; + let nextChar = pathname.charAt(startIndex); if (nextChar && nextChar !== "/") { // pathname does not start with basename/ return null; } - return pathname.slice(basename.length) || "/"; + return pathname.slice(startIndex) || "/"; } /**