diff --git a/.changeset/relative-splat-path.md b/.changeset/relative-splat-path.md new file mode 100644 index 00000000000..711a5811339 --- /dev/null +++ b/.changeset/relative-splat-path.md @@ -0,0 +1,8 @@ +--- +"@remix-run/dev": minor +"@remix-run/react": minor +"@remix-run/server-runtime": minor +"@remix-run/testing": minor +--- + +Add a new `future.v3_relativeSplatPath` flag to implement a breaking bug fix to relative routing when inside a splat route. For more information, please see the React Router [`6.21.0` Release Notes](https://github.com/remix-run/react-router/blob/release-next/CHANGELOG.md#futurev7_relativesplatpath) and the [`useResolvedPath` docs](https://remix.run/hooks/use-resolved-path#splat-paths). diff --git a/docs/components/form.md b/docs/components/form.md index 1037ef6ae89..75186ded127 100644 --- a/docs/components/form.md +++ b/docs/components/form.md @@ -27,6 +27,8 @@ The URL to submit the form data to. If `undefined`, this defaults to the closest route in context. If a parent route renders a `
` but the URL matches deeper child routes, the form will post to the parent route. Likewise, a form inside the child route will post to the child route. This differs from a native [``][form_element] that will always point to the full URL. +Please see the [Splat Paths][relativesplatpath] section on the `useResolvedPath` docs for a note on the behavior of the `future.v3_relativeSplatPath` future flag for relative `` behavior within splat routes + ### `method` This determines the [HTTP verb][http_verb] to be used: `DELETE`, `GET`, `PATCH`, `POST`, and `PUT`. The default is `GET`. @@ -156,3 +158,4 @@ See also: [view-transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API [document-start-view-transition]: https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition [use-view-transition-state]: ../hooks/use-view-transition-state +[relativesplatpath]: ../hooks/use-resolved-path#splat-paths diff --git a/docs/components/link.md b/docs/components/link.md index 08ab2558813..57531bac089 100644 --- a/docs/components/link.md +++ b/docs/components/link.md @@ -12,6 +12,8 @@ import { Link } from "@remix-run/react"; Dashboard; ``` +Please see the [Splat Paths][relativesplatpath] section on the `useResolvedPath` docs for a note on the behavior of the `future.v3_relativeSplatPath` future flag for relative `` behavior within splat routes + ## Props ### `prefetch` @@ -197,3 +199,4 @@ Please note that this API is marked unstable and may be subject to breaking chan [view-transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API [document-start-view-transition]: https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition [use-view-transition-state]: ../hooks/use-view-transition-state +[relativesplatpath]: ../hooks/use-resolved-path#splat-paths diff --git a/docs/hooks/use-href.md b/docs/hooks/use-href.md index 72c1eca096d..7782d304ff9 100644 --- a/docs/hooks/use-href.md +++ b/docs/hooks/use-href.md @@ -26,6 +26,8 @@ useHref(to, options) Optional. The path to append to the resolved URL. +Please see the [Splat Paths][relativesplatpath] section on the `useResolvedPath` docs for a note on the behavior of the `future.v3_relativeSplatPath` future flag for relative `useHref()` behavior within splat routes + ### `options` The only option is `{ relative: "route" | "path"}`, which defines the behavior when resolving relative URLs. @@ -35,3 +37,4 @@ The only option is `{ relative: "route" | "path"}`, which defines the behavior w [anchor_element_href_attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#href [anchor_element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link +[relativesplatpath]: ./use-resolved-path#splat-paths diff --git a/docs/hooks/use-navigate.md b/docs/hooks/use-navigate.md index 1a9aabb53c1..03cc3b70091 100644 --- a/docs/hooks/use-navigate.md +++ b/docs/hooks/use-navigate.md @@ -40,6 +40,8 @@ navigate(".."); navigate("../other/path"); ``` +Please see the [Splat Paths][relativesplatpath] section on the `useResolvedPath` docs for a note on the behavior of the `future.v3_relativeSplatPath` future flag for relative `useNavigate()` behavior within splat routes + ### `to: To` You can also pass a `To` value: @@ -91,3 +93,4 @@ navigate(".", { [use-view-transition-state]: ../hooks//use-view-transition-state [action]: ../route/action [loader]: ../route/loader +[relativesplatpath]: ./use-resolved-path#splat-paths diff --git a/docs/hooks/use-resolved-path.md b/docs/hooks/use-resolved-path.md index 87511c5d03d..a309689ef49 100644 --- a/docs/hooks/use-resolved-path.md +++ b/docs/hooks/use-resolved-path.md @@ -20,9 +20,31 @@ function SomeComponent() { This is useful when building links from relative values and used internally for [``][nav-link-component]. +## Splat Paths + +The original logic for React Router's `useResolvedPath` hook behaved differently for splat paths which in hindsight was incorrect/buggy behavior. Please see the [React Router Docs][rr-use-resolved-path-splat] for a longer explanation but this was determined to be a "breaking bug fix" and thus was fixed behind a future flag in React Router and exposed up through Remix's future flags. This will become the default behavior in Remix v3, so it is recommended to update your applications at your convenience to be better prepared for the eventual v3 upgrade. + +It should be noted that this is the foundation for all relative routing in Remix, so this applies to the following relative path code flows as well: + +- `` +- `useNavigate()` +- `useHref()` +- `` +- `useSubmit()` +- Relative path `redirect` responses returned from loaders and actions + +### Behavior without the flag + +When this flag is not enabled, the default behavior is that when resolving relative paths inside of a splat route, the splat portion of the path is ignored. So, within a `routes/dashboard.$.tsx` file, `useResolvedPath(".")` would resolve to `/dashboard` even if the current URL was `/dashboard/teams`. + +### Behavior with the flag + +When you enable the flag, this "bug" is fixed so that path resolution is consistent across all route types, and `useResolvedPath(".")` always resolves to the current pathname for the contextual route. This includes any dynamic param or splat param values, so within a `routes/dashboard.$.tsx` file, `useResolvedPath(".")` would resolve to `/dashboard/teams` when the current URL was `/dashboard/teams`. + ## Additional Resources - [`resolvePath`][rr-resolve-path] [nav-link-component]: ../components/nav-link [rr-resolve-path]: https://reactrouter.com/utils/resolve-path +[rr-use-resolved-path-splat]: https://reactrouter.com/hooks/use-resolved-path#splat-paths diff --git a/docs/hooks/use-submit.md b/docs/hooks/use-submit.md index cff938800b7..0a5b7ba9fb4 100644 --- a/docs/hooks/use-submit.md +++ b/docs/hooks/use-submit.md @@ -82,6 +82,8 @@ submit(data, { }); ``` +Please see the [Splat Paths][relativesplatpath] section on the `useResolvedPath` docs for a note on the behavior of the `future.v3_relativeSplatPath` future flag for relative `useSubmit()` behavior within splat routes + ## Additional Resources **Discussion** @@ -102,3 +104,4 @@ submit(data, { [start-transition]: https://react.dev/reference/react/startTransition [view-transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API [use-view-transition-state]: ../hooks//use-view-transition-state +[relativesplatpath]: ./use-resolved-path#splat-paths diff --git a/integration/form-test.ts b/integration/form-test.ts index b2c5ead7a2b..f2331ad2987 100644 --- a/integration/form-test.ts +++ b/integration/form-test.ts @@ -921,7 +921,7 @@ test.describe("Forms", () => { await app.goto("/projects/blarg"); let html = await app.getHtml(); let el = getElement(html, `#${SPLAT_ROUTE_NO_ACTION}`); - expect(el.attr("action")).toMatch("/projects/blarg"); + expect(el.attr("action")).toMatch("/projects"); }); test("no action resolves to URL including search params", async ({ @@ -931,7 +931,7 @@ test.describe("Forms", () => { await app.goto("/projects/blarg?foo=bar"); let html = await app.getHtml(); let el = getElement(html, `#${SPLAT_ROUTE_NO_ACTION}`); - expect(el.attr("action")).toMatch("/projects/blarg?foo=bar"); + expect(el.attr("action")).toMatch("/projects?foo=bar"); }); test("absolute action resolves relative to the root route", async ({ diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index d13f25bf1ac..1115c7bb6db 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -37,6 +37,7 @@ describe("readConfig", () => { "entryServerFilePath": Any, "future": { "v3_fetcherPersist": false, + "v3_relativeSplatPath": false, }, "mdx": undefined, "postcss": true, diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index b3b2a25fc44..bc97d243ed3 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -35,6 +35,7 @@ type Dev = { interface FutureConfig { v3_fetcherPersist: boolean; + v3_relativeSplatPath: boolean; } type NodeBuiltinsPolyfillOptions = Pick< @@ -581,6 +582,7 @@ export async function resolveConfig( // typings this won't be necessary anymore. let future: FutureConfig = { v3_fetcherPersist: appConfig.future?.v3_fetcherPersist === true, + v3_relativeSplatPath: appConfig.future?.v3_relativeSplatPath === true, }; if (appConfig.future) { diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 5d398d1a049..a8a7967f30b 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -29,7 +29,7 @@ "@mdx-js/mdx": "^2.3.0", "@npmcli/package-json": "^4.0.1", "@remix-run/node": "2.3.1", - "@remix-run/router": "0.0.0-experimental-35fa15e5", + "@remix-run/router": "1.14.0-pre.0", "@remix-run/server-runtime": "2.3.1", "@types/mdx": "^2.0.5", "@vanilla-extract/integration": "^6.2.0", diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index c7b8eef1e37..168cead8f34 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -349,6 +349,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { relativeAssetsBuildDirectory, future: { v3_fetcherPersist: options.future?.v3_fetcherPersist === true, + v3_relativeSplatPath: options.future?.v3_relativeSplatPath === true, }, }; }; diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index 0d504ba6558..ebab7f75470 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -260,6 +260,7 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { v7_fetcherPersist: window.__remixContext.future.v3_fetcherPersist, v7_partialHydration: true, v7_prependBasename: true, + v7_relativeSplatPath: window.__remixContext.future.v3_relativeSplatPath, }, hydrationData, mapRouteProperties, diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index c1ef0b47288..93b93efaa75 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -27,6 +27,7 @@ export interface EntryContext extends RemixContextObject { export interface FutureConfig { v3_fetcherPersist: boolean; + v3_relativeSplatPath: boolean; } export interface AssetsManifest { diff --git a/packages/remix-react/package.json b/packages/remix-react/package.json index d7085ead805..1fe9d375e33 100644 --- a/packages/remix-react/package.json +++ b/packages/remix-react/package.json @@ -16,10 +16,10 @@ "typings": "dist/index.d.ts", "module": "dist/esm/index.js", "dependencies": { - "@remix-run/router": "0.0.0-experimental-35fa15e5", + "@remix-run/router": "1.14.0-pre.0", "@remix-run/server-runtime": "2.3.1", - "react-router": "0.0.0-experimental-35fa15e5", - "react-router-dom": "0.0.0-experimental-35fa15e5" + "react-router": "6.21.0-pre.0", + "react-router-dom": "6.21.0-pre.0" }, "devDependencies": { "@testing-library/jest-dom": "^5.17.0", diff --git a/packages/remix-react/server.tsx b/packages/remix-react/server.tsx index e20c7c817db..8d5b581e57c 100644 --- a/packages/remix-react/server.tsx +++ b/packages/remix-react/server.tsx @@ -68,6 +68,7 @@ export function RemixServer({ let router = createStaticRouter(routes, context.staticHandlerContext, { future: { v7_partialHydration: true, + v7_relativeSplatPath: context.future.v3_relativeSplatPath, }, }); diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index 6a03ea5c901..6b0b0799322 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -16,6 +16,7 @@ export interface EntryContext { export interface FutureConfig { v3_fetcherPersist: boolean; + v3_relativeSplatPath: boolean; } export interface AssetsManifest { diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 290a4d2f955..380565f5bcc 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -16,7 +16,7 @@ "typings": "dist/index.d.ts", "module": "dist/esm/index.js", "dependencies": { - "@remix-run/router": "0.0.0-experimental-35fa15e5", + "@remix-run/router": "1.14.0-pre.0", "@types/cookie": "^0.5.3", "@web3-storage/multipart-parser": "^1.0.0", "cookie": "^0.5.0", diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index a0e1900df58..01a19a31aa7 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -44,7 +44,11 @@ function derive(build: ServerBuild, mode?: string) { let routes = createRoutes(build.routes); let dataRoutes = createStaticHandlerDataRoutes(build.routes, build.future); let serverMode = isServerMode(mode) ? mode : ServerMode.Production; - let staticHandler = createStaticHandler(dataRoutes); + let staticHandler = createStaticHandler(dataRoutes, { + future: { + v7_relativeSplatPath: build.future?.v3_relativeSplatPath, + }, + }); let errorHandler = build.entry.module.handleError || diff --git a/packages/remix-testing/create-remix-stub.tsx b/packages/remix-testing/create-remix-stub.tsx index ee35e1c62aa..b5fd4222e81 100644 --- a/packages/remix-testing/create-remix-stub.tsx +++ b/packages/remix-testing/create-remix-stub.tsx @@ -105,6 +105,7 @@ export function createRemixStub( remixContextRef.current = { future: { v3_fetcherPersist: future?.v3_fetcherPersist === true, + v3_relativeSplatPath: future?.v3_relativeSplatPath === true, }, manifest: { routes: {}, diff --git a/packages/remix-testing/package.json b/packages/remix-testing/package.json index 39be714428a..e8d1cc7ced9 100644 --- a/packages/remix-testing/package.json +++ b/packages/remix-testing/package.json @@ -18,8 +18,8 @@ "dependencies": { "@remix-run/node": "2.3.1", "@remix-run/react": "2.3.1", - "@remix-run/router": "0.0.0-experimental-35fa15e5", - "react-router-dom": "0.0.0-experimental-35fa15e5" + "@remix-run/router": "1.14.0-pre.0", + "react-router-dom": "6.21.0-pre.0" }, "devDependencies": { "@types/node": "^18.17.1", diff --git a/yarn.lock b/yarn.lock index ee21bd28a31..3986c421a53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2314,10 +2314,10 @@ "@changesets/types" "^5.0.0" dotenv "^8.1.0" -"@remix-run/router@0.0.0-experimental-35fa15e5": - version "0.0.0-experimental-35fa15e5" - resolved "https://registry.npmjs.org/@remix-run/router/-/router-0.0.0-experimental-35fa15e5.tgz#f23be11e4d7b7a4260125c70827061882dadb61a" - integrity sha512-pwZ538/KJD18v0OzOPjLrBz1adpJ1I/+ounD1zckjAkBp2Jpm+oQKfzmh/58tryxwMete9Ij7vG0thz5aveAFg== +"@remix-run/router@1.14.0-pre.0": + version "1.14.0-pre.0" + resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.14.0-pre.0.tgz#e19bd909a4dcd6682f08ad62691b6663eea3b0cd" + integrity sha512-AbyTs3orZCJaR1xKPHB7ovMa/kIDeaKwcrYNsXhFlbSB40PHmISmUPiCM7+ZRBZ70yN/ilEgDxhxHO8+w+kZMg== "@remix-run/web-blob@^3.1.0": version "3.1.0" @@ -11024,20 +11024,20 @@ react-refresh@^0.14.0: resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz" integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== -react-router-dom@0.0.0-experimental-35fa15e5: - version "0.0.0-experimental-35fa15e5" - resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-0.0.0-experimental-35fa15e5.tgz#eb8ff2547c3c047d21f38903083d6d9148a56c90" - integrity sha512-g+82k1K2aeSvWp+TuR/P1pvnxtXRwAJvEULCeX0u63JitO7tT3t2pE8QaRIfrhHAoeubZFwXHcAmlqg8xufsUA== +react-router-dom@6.21.0-pre.0: + version "6.21.0-pre.0" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.0-pre.0.tgz#d83dce6cd4138f812c56de38a8c8fe5904875f67" + integrity sha512-B0bge8t6MmtUhvCRd8GN49DYXT6xqP0K/Twv2XfXcR3vFFkcnRPs20yTanNvbROljQJJInRgjj0jqWH5BVPfEQ== dependencies: - "@remix-run/router" "0.0.0-experimental-35fa15e5" - react-router "0.0.0-experimental-35fa15e5" + "@remix-run/router" "1.14.0-pre.0" + react-router "6.21.0-pre.0" -react-router@0.0.0-experimental-35fa15e5: - version "0.0.0-experimental-35fa15e5" - resolved "https://registry.npmjs.org/react-router/-/react-router-0.0.0-experimental-35fa15e5.tgz#81a5e95aa976ce565cb980fc16df90954299b2f5" - integrity sha512-F+IvQVWQWmsTUvpUXBl3FdgyT+GUCJmHmEKeljo7e792UfJ51j5kwSttuAREwuDX4z5D8JRMnF6/grIHp/AT/A== +react-router@6.21.0-pre.0: + version "6.21.0-pre.0" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.21.0-pre.0.tgz#1569190717723cc454078c12a08cebfbb8cb71ef" + integrity sha512-ec9JqPyoGwT3x5lFDbh9RrD7UD+E0ygRvkItIZefnt8y9ilB1ovbSNYApz5FZ4erTvWpvQz22SoZq5fkB9vGVQ== dependencies: - "@remix-run/router" "0.0.0-experimental-35fa15e5" + "@remix-run/router" "1.14.0-pre.0" react@^18.2.0: version "18.2.0"