|
| 1 | +--- |
| 2 | +"react-router-dom-v5-compat": minor |
| 3 | +"react-router-native": minor |
| 4 | +"react-router-dom": minor |
| 5 | +"react-router": minor |
| 6 | +"@remix-run/router": minor |
| 7 | +--- |
| 8 | + |
| 9 | +Add a new `future.v7_relativeSplatPath` flag to implenent a breaking bug fix to relative routing when inside a splat route. |
| 10 | + |
| 11 | +This fix was originally added in [#10983](https://github.com/remix-run/react-router/issues/10983) and was later reverted in [#11078](https://github.com/remix-run/react-router/issues/110788) because it was determined that a large number of existing applications were relying on the buggy behavior (see [#11052](https://github.com/remix-run/react-router/issues/11052)) |
| 12 | + |
| 13 | +**The Bug** |
| 14 | +The buggy behavior is that without this flag, the default behavior when resolving relative paths is to _ignore_ any splat (`*`) portion of the current route path. |
| 15 | + |
| 16 | +**The Background** |
| 17 | +This decision was originally made thinking that it would make the concept of nested different sections of your apps in `<Routes>` easier if relative routing would _replace_ the current splat: |
| 18 | + |
| 19 | +```jsx |
| 20 | +<BrowserRouter> |
| 21 | + <Routes> |
| 22 | + <Route path="/" element={<Home />} /> |
| 23 | + <Route path="dashboard/*" element={<Dashboard />} /> |
| 24 | + </Routes> |
| 25 | +</BrowserRouter> |
| 26 | +``` |
| 27 | + |
| 28 | +Any paths like `/dashboard`, `/dashboard/team`, `/dashboard/projects` will match the `Dashboard` route. The dashboard component itself can then render nested `<Routes>`: |
| 29 | + |
| 30 | +```jsx |
| 31 | +function Dashboard() { |
| 32 | + return ( |
| 33 | + <div> |
| 34 | + <h2>Dashboard</h2> |
| 35 | + <nav> |
| 36 | + <Link to="/">Dashboard Home</Link> |
| 37 | + <Link to="team">Team</Link> |
| 38 | + <Link to="projects">Projects</Link> |
| 39 | + </nav> |
| 40 | + |
| 41 | + <Routes> |
| 42 | + <Route path="/" element={<DashboardHome />} /> |
| 43 | + <Route path="team" element={<DashboardTeam />} /> |
| 44 | + <Route path="projects" element={<DashboardProjects />} /> |
| 45 | + </Routes> |
| 46 | + </div> |
| 47 | + ); |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +Now, all links and route paths are relative to the router above them. This makes code splitting and compartmentalizing your app really easy. You could render the `Dashboard` as its own independent app, or embed it into your large app without making any changes to it. |
| 52 | + |
| 53 | +**The Problem** |
| 54 | + |
| 55 | +The problem is that this concept of ignoring part of a pth breaks a lot of other assumptions in React Router - namely that `"."` always means the current location pathname for that route. When we ignore the splat portion, we start getting invalid paths when using `"."`: |
| 56 | + |
| 57 | +```jsx |
| 58 | +// If we are on URL /dashboard/team, and we want to link to /dashboard/team: |
| 59 | +function DashboardTeam() { |
| 60 | + // ❌ This is broken and results in <a href="/dashboard"> |
| 61 | + return <Link to=".">A broken link to the Current URL</Link>; |
| 62 | + |
| 63 | + // ✅ This is fixed but super unintuitive since we're already at /dashboard/team! |
| 64 | + return <Link to="./team">A broken link to the Current URL</Link>; |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +We've also introduced an issue that we can no longer move our `DashboardTeam` component around our route hierarchy easily - since it behaves differently if we're underneath a non-splat route, such as `/dashboard/:widget`. Now, our `"."` links will, properly point to ourself _inclusive of the dynamic param value_ so behavior will break from it's corresponding usage in a `/dashboard/*` route. |
| 69 | + |
| 70 | +Even worse, consider a nested splat route configuration: |
| 71 | + |
| 72 | +```jsx |
| 73 | +<BrowserRouter> |
| 74 | + <Routes> |
| 75 | + <Route path="dashboard"> |
| 76 | + <Route path="*" element={<Dashboard />} /> |
| 77 | + </Route> |
| 78 | + </Routes> |
| 79 | +</BrowserRouter> |
| 80 | +``` |
| 81 | + |
| 82 | +Now, a `<Link to=".">` and a `<Link to="..">` inside the `Dashboard` component go to the same place! That is definitely not correct! |
| 83 | + |
| 84 | +Another common issue arose in Data Routers (and Remix) where any `<Form>` should post to it's own route `action` if you the user doesn't specify a form action: |
| 85 | + |
| 86 | +```jsx |
| 87 | +let router = createBrowserRouter({ |
| 88 | + path: "/dashboard", |
| 89 | + children: [ |
| 90 | + { |
| 91 | + path: "*", |
| 92 | + action: dashboardAction, |
| 93 | + Component() { |
| 94 | + // ❌ This form is broken! It throws a 405 error when it submits because |
| 95 | + // it tries to submit to /dashboard (without the splat value) and the parent |
| 96 | + // `/dashboard` route doesn't have an action |
| 97 | + return <Form method="post">...</Form>; |
| 98 | + }, |
| 99 | + }, |
| 100 | + ], |
| 101 | +}); |
| 102 | +``` |
| 103 | + |
| 104 | +This is just a compounded issue from the above because the default location for a `Form` to submit to is itself (`"."`) - and if we ignore the splat portion, that now resolves to the parent route. |
| 105 | + |
| 106 | +**The Solution** |
| 107 | +If you are leveraging this behavior, it's recommended to enable the future flag, move your splat to it's own route, and leverage `../` for any links to "sibling" pages: |
| 108 | + |
| 109 | +```jsx |
| 110 | +<BrowserRouter> |
| 111 | + <Routes> |
| 112 | + <Route path="dashboard"> |
| 113 | + <Route path="*" element={<Dashboard />} /> |
| 114 | + </Route> |
| 115 | + </Routes> |
| 116 | +</BrowserRouter> |
| 117 | + |
| 118 | +function Dashboard() { |
| 119 | + return ( |
| 120 | + <div> |
| 121 | + <h2>Dashboard</h2> |
| 122 | + <nav> |
| 123 | + <Link to="..">Dashboard Home</Link> |
| 124 | + <Link to="../team">Team</Link> |
| 125 | + <Link to="../projects">Projects</Link> |
| 126 | + </nav> |
| 127 | + |
| 128 | + <Routes> |
| 129 | + <Route path="/" element={<DashboardHome />} /> |
| 130 | + <Route path="team" element={<DashboardTeam />} /> |
| 131 | + <Route path="projects" element={<DashboardProjects />} /> |
| 132 | + </Router> |
| 133 | + </div> |
| 134 | + ); |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +This way, `.` means "the full current pathname for my route" in all cases (including static, dynamic, and splat routes) and `..` always means "my parents pathname". |
0 commit comments