@remix-run/routerresolvePath (#14537)fetcherKey as a parameter to patchRoutesOnNavigation (#13109)6.29.0 via #12169 that caused issues navigating to hash routes inside splat routes for applications using Lazy Route Discovery (patchRoutesOnNavigation) (#13108)Provide the request signal as a parameter to patchRoutesOnNavigation (#12900)
patchRoutesOnNavigation path param for fetcher calls (#12899)data() result (#12845)matchRoutes calls when possible (#12169)future.v7_fetcherPersistjson/defer in favor of returning raw objectsunstable_patchRoutesOnNavigation (#11973)
PatchRoutesOnNavigationFunctionArgs type for convenience (#11967)unstable_dataStrategy (#11974)unstable_flushSync option for navigations and fetchers (#11989)unstable_viewTransition option for navigations and the corresponding unstable_useViewTransitionState hook (#11989)?index param already exists from a prior submission (#12003)useFormAction bug - when removing ?index param it would not keep other non-Remix index params (#12003)preventScrollReset through redirects during concurrent fetches (#11999)patchRoutesOnNavigation calls (#12055)
patchRoutesOnNavigation internally so that multiple navigations with the same start/end would only execute the function once and use the same promisepatch short circuiting if a navigation was interrupted (and the request.signal aborted) since the first invocation's patch would no-opimport() for async routes will already be cached automatically - and if not it's easy enough for users to implement this cache in userlandconsole.error on fetcher abort due to back-to-back revalidation calls (#12050)patchRoutesOnNavigation directly to useRouteError instead of wrapping them in a 400 ErrorResponse instance (#12111)RouteObject within PatchRoutesOnNavigationFunction's patch method so it doesn't expect agnostic route objects passed to patch (#11967)partialHydration when hydrating with errors (#12070)discoveredRoutes FIFO queue from unstable_patchRoutesOnNavigation (#11977)unstable_dataStrategy API to allow for more advanced implementations (#11943)
unstable_HandlerResult to unstable_DataStrategyResultunstable_DataStrategyResult[] (parallel to matches) to a key/value object of routeId => unstable_DataStrategyResultmatch.shouldLoad)unstable_dataStrategyfetcherKey parameter to unstable_dataStrategy to allow differentiation from navigational and fetcher callshandlerOverride instead of returning a DataStrategyResultmatch.resolve() into a final results object you should not need to think about the DataStrategyResult typehandlerOverride, then you will need to assign a DataStrategyResult as the value so React Router knows if it's a successful execution or an error.blocker.proceed is called quickly/synchronously (#11930)Fog of War: Update unstable_patchRoutesOnMiss logic so that we call the method when we match routes with dynamic param or splat segments in case there exists a higher-scoring static route that we've not yet discovered. (#11883)
unstable_patchRouteOnMiss against so that we don't re-call on subsequent navigations to the same pathRename unstable_patchRoutesOnMiss to unstable_patchRoutesOnNavigation to match new behavior (#11888)
replace(url, init?) alternative to redirect(url, init?) that performs a history.replaceState instead of a history.pushState on client-side navigation redirects (#11811)unstable_data() API for usage with Remix Single Fetch (#11836)
createStaticHandler.query() to allow loaders/actions to return arbitrary data + status/headers without forcing the serialization of data into a Response instanceunstable_dataStrategy such as serializing via turbo-stream in Remix Single Fetchstatus field from HandlerResultstatus from unstable_dataStrategy you should instead do so via unstable_data()fetcher.load is interrupted by an action submission, we track it internally and force revalidation once the action completesfetcher.load was interrupted by a fetcher.submit, then we wouldn't remove it from this internal tracking info on successful load (incorrectly)fetcher.load again, ignoring any shouldRevalidate logicfuture.v7_partialHydration along with unstable_patchRoutesOnMiss (#11838)
router.state.matches will now include any partial matches so that we can render ancestor HydrateFallback componentsfuture.unstable_skipActionErrorRevalidation as future.v7_skipActionErrorRevalidation (#11769)
Response with a 4xx/5xx status codeshouldRevalidateshouldRevalidate's unstable_actionStatus parameter to actionStatusunstable_patchRoutesOnMiss (#11786)unstable_patchRoutesOnMiss that matched a splat route on the server (#11790)router.routes identity/reflow during route patching (#11740)Add support for Lazy Route Discovery (a.k.a. Fog of War) (#11626)
unstable_patchRoutesOnMiss docs: https://reactrouter.com/v6/routers/create-browser-routerunstable_dataStrategy on staticHandler.queryRoute (#11515)unstable_dataStrategy configuration option (#11098)
unstable_dataStrategy from createStaticHandler to staticHandler.query so it can be request-specific for use with the ResponseStub approach in Remix. It's not really applicable to queryRoute for now since that's a singular handler call anyway so any pre-processing/post/processing could be done there manually. (#11377)future.unstable_skipActionRevalidation future flag (#11098)
true from shouldRevalidateshouldRevalidate now also receives a new unstable_actionStatus argument alongside actionResult so you can make decision based on the status of the action response without having to encode it into the action dataskipLoaderErrorBubbling flag to staticHandler.query to disable error bubbling on loader executions for single-fetch scenarios where the client-side router will handle the bubbling (#11098)future.v7_partialHydration bug that would re-run loaders below the boundary on hydration if SSR loader errors bubbled to a parent boundary (#11324)future.v7_partialHydration bug that would consider the router uninitialized if a route did not have a loader (#11325)Add a createStaticHandler future.v7_throwAbortReason flag to throw request.signal.reason (defaults to a DOMException) when a request is aborted instead of an Error such as new Error("query() call aborted: GET /path") (#11104)
DOMException was added in Node v17 so you will not get a DOMException on Node 16 and below.ErrorResponse status code if passed to getStaticContextFormError (#11213)route.lazy not working correctly on initial SPA load when v7_partialHydration is specified (#11121)submitting phase (#11102)resolveTo (#11097)future.v7_partialHydration future flag that enables partial hydration of a data router when Server-Side Rendering. This allows you to provide hydrationData.loaderData that has values for some initially matched route loaders, but not all. When this flag is enabled, the router will call loader functions for routes that do not have hydration loader data during router.initialize(), and it will render down to the deepest provided HydrateFallback (up to the first route without hydration data) while it executes the unhydrated routes. (#11033)For example, the following router has a root and index route, but only provided hydrationData.loaderData for the root route. Because the index route has a loader, we need to run that during initialization. With future.v7_partialHydration specified, <RouterProvider> will render the RootComponent (because it has data) and then the IndexFallback (since it does not have data). Once indexLoader finishes, application will update and display IndexComponent.
let router = createBrowserRouter(
[
{
id: "root",
path: "/",
loader: rootLoader,
Component: RootComponent,
Fallback: RootFallback,
children: [
{
id: "index",
index: true,
loader: indexLoader,
Component: IndexComponent,
HydrateFallback: IndexFallback,
},
],
},
],
{
future: {
v7_partialHydration: true,
},
hydrationData: {
loaderData: {
root: { message: "Hydrated from Root!" },
},
},
}
);
If the above example did not have an IndexFallback, then RouterProvider would instead render the RootFallback while it executed the indexLoader.
Note: When future.v7_partialHydration is provided, the <RouterProvider fallbackElement> prop is ignored since you can move it to a Fallback on your top-most route. The fallbackElement prop will be removed in React Router v7 when v7_partialHydration behavior becomes the standard behavior.
future.v7_relativeSplatPath flag to implement a breaking bug fix to relative routing when inside a splat route. (#11087)This fix was originally added in #10983 and was later reverted in #11078 because it was determined that a large number of existing applications were relying on the buggy behavior (see #11052)
The Bug
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.
The Background
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:
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="dashboard/*" element={<Dashboard />} />
</Routes>
</BrowserRouter>
Any paths like /dashboard, /dashboard/team, /dashboard/projects will match the Dashboard route. The dashboard component itself can then render nested <Routes>:
function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
<nav>
<Link to="/">Dashboard Home</Link>
<Link to="team">Team</Link>
<Link to="projects">Projects</Link>
</nav>
<Routes>
<Route path="/" element={<DashboardHome />} />
<Route path="team" element={<DashboardTeam />} />
<Route path="projects" element={<DashboardProjects />} />
</Routes>
</div>
);
}
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.
The Problem
The problem is that this concept of ignoring part of a path 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 ".":
// If we are on URL /dashboard/team, and we want to link to /dashboard/team:
function DashboardTeam() {
// ❌ This is broken and results in <a href="/dashboard">
return <Link to=".">A broken link to the Current URL</Link>;
// ✅ This is fixed but super unintuitive since we're already at /dashboard/team!
return <Link to="./team">A broken link to the Current URL</Link>;
}
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.
Even worse, consider a nested splat route configuration:
<BrowserRouter>
<Routes>
<Route path="dashboard">
<Route path="*" element={<Dashboard />} />
</Route>
</Routes>
</BrowserRouter>
Now, a <Link to="."> and a <Link to=".."> inside the Dashboard component go to the same place! That is definitely not correct!
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:
let router = createBrowserRouter({
path: "/dashboard",
children: [
{
path: "*",
action: dashboardAction,
Component() {
// ❌ This form is broken! It throws a 405 error when it submits because
// it tries to submit to /dashboard (without the splat value) and the parent
// `/dashboard` route doesn't have an action
return <Form method="post">...</Form>;
},
},
],
});
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.
The Solution
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:
<BrowserRouter>
<Routes>
<Route path="dashboard">
<Route index path="*" element={<Dashboard />} />
</Route>
</Routes>
</BrowserRouter>
function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
<nav>
<Link to="..">Dashboard Home</Link>
<Link to="../team">Team</Link>
<Link to="../projects">Projects</Link>
</nav>
<Routes>
<Route path="/" element={<DashboardHome />} />
<Route path="team" element={<DashboardTeam />} />
<Route path="projects" element={<DashboardProjects />} />
</Router>
</div>
);
}
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".
loader/action functions (#11061)relative="path" issue when rendering Link/NavLink outside of matched routes (#11062)useResolvedPath fix for splat routes due to a large number of applications that were relying on the buggy behavior (see https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329). We plan to re-introduce this fix behind a future flag in the next minor version. (#11078)PathParam type from the public API (#10719)resolveTo in splat routes (#11045)
getPathContributingMatchesUNSAFE_getPathContributingMatches export from @remix-run/router since we no longer need this in the react-router/react-router-dom layersv7_fetcherPersist is enabled (#11044)unstable_flushSync option to router.navigate and router.fetch to tell the React Router layer to opt-out of React.startTransition and into ReactDOM.flushSync for state updates (#11005)relative="path" bug where relative path calculations started from the full location pathname, instead of from the current contextual route pathname. (#11006) <Route path="/a">
<Route path="/b" element={<Component />}>
<Route path="/c" />
</Route>
</Route>;
function Component() {
return (
<>
{/* This is now correctly relative to /a/b, not /a/b/c */}
<Link to=".." relative="path" />
<Outlet />
</>
);
}
Add a new future.v7_fetcherPersist flag to the @remix-run/router to change the persistence behavior of fetchers when router.deleteFetcher is called. Instead of being immediately cleaned up, fetchers will persist until they return to an idle state (RFC) (#10962)
useFetchers() API was always supposed to only reflect in-flight fetcher information for pending/optimistic UI -- it was not intended to reflect fetcher data or hang onto fetchers after they returned to an idle stateuseFetchers(). They served effectively no purpose in there since you can access the data via useFetcher().data).idle state. They will remain exposed via useFetchers while in-flight so you can still access pending/optimistic data after unmount.When v7_fetcherPersist is enabled, the router now performs ref-counting on fetcher keys via getFetcher/deleteFetcher so it knows when a given fetcher is totally unmounted from the UI (#10977)
deletedFetchers array to the subscriber callbacks so that the UI layer can remove associated fetcher dataAdd support for optional path segments in matchPath (#10768)
router.getFetcher/router.deleteFetcher type definitions which incorrectly specified key as an optional parameter (#10960)unstable_viewTransition option to router.navigate (#10916)ErrorResponse type to avoid leaking internal field (#10876)any with unknown on exposed typings for user-provided data. To do this in Remix v2 without introducing breaking changes in React Router v6, we have added generics to a number of shared types. These continue to default to any in React Router and are overridden with unknown in Remix. In React Router v7 we plan to move these to unknown as a breaking change. (#10843)
Location now accepts a generic for the location.state valueActionFunctionArgs/ActionFunction/LoaderFunctionArgs/LoaderFunction now accept a generic for the context parameter (only used in SSR usages via createStaticHandler)useMatches (now exported as UIMatch) accepts generics for match.data and match.handle - both of which were already set to unknown@private class export ErrorResponse to an UNSAFE_ErrorResponseImpl export since it is an implementation detail and there should be no construction of ErrorResponse instances in userland. This frees us up to export a type ErrorResponse which correlates to an instance of the class via InstanceType. Userland code should only ever be using ErrorResponse as a type and should be type-narrowing via isRouteErrorResponse. (#10811)ShouldRevalidateFunctionArgs interface (#10797)_isFetchActionRedirect, _hasFetcherDoneAnything) (#10715)query/queryRoute calls (#10793)route.lazy routes (#10778)actionResult on the arguments object passed to shouldRevalidate (#10779)redirectDocument() function which allows users to specify that a redirect from a loader/action should trigger a document reload (via window.location) instead of attempting to navigate to the redirected location via React Router (#10705)queryRoute that was not always identifying thrown Response instances (#10717)defer promise resolves/rejects with undefined in order to match the behavior of loaders and actions which must return a value or null (#10690)Route.lazy to prohibit returning an empty object (#10634)application/json and text/plain encodings for router.navigate/router.fetch submissions. To leverage these encodings, pass your data in a body parameter and specify the desired formEncType: (#10413) // By default, the encoding is "application/x-www-form-urlencoded"
router.navigate("/", {
formMethod: "post",
body: { key: "value" },
});
async function action({ request }) {
// await request.formData() => FormData instance with entry [key=value]
}
// Pass `formEncType` to opt-into a different encoding (json)
router.navigate("/", {
formMethod: "post",
formEncType: "application/json",
body: { key: "value" },
});
async function action({ request }) {
// await request.json() => { key: "value" }
}
// Pass `formEncType` to opt-into a different encoding (text)
router.navigate("/", {
formMethod: "post",
formEncType: "text/plain",
body: "Text submission",
});
async function action({ request }) {
// await request.text() => "Text submission"
}
window.history.pushState/replaceState before updating React Router state (instead of after) so that window.location matches useLocation during synchronous React 17 rendering (#10448)
window.location and should always reference useLocation when possible, as window.location will not be in sync 100% of the time (due to popstate events, concurrent mode, etc.)basename from the location provided to <ScrollRestoration getKey> to match the useLocation behavior (#10550)shouldRevalidate for fetchers that have not yet completed a data load (#10623)unstable_useBlocker key issues in StrictMode (#10573)typescript to 5.1 (#10581)DOMException (DataCloneError) when attempting to perform a PUSH navigation with non-serializable state. (#10427)manifest in \_internalSetRoutes (#10437)basename handling when navigating without a path (#10433)/path#hash -> /path#hash) (#10408)Enable relative routing in the @remix-run/router when providing a source route ID from which the path is relative to: (#10336)
router.navigate("../path", { fromRouteId: "some-route" }).router.fetch which already receives a source route IDIntroduce a new @remix-run/router future.v7_prependBasename flag to enable basename prefixing to all paths coming into router.navigate and router.fetch.
basename was prepended in the React Router layer, but now that relative routing is being handled by the router we need prepend the basename after resolving any relative pathsbasename support in useFetcher as wellLoaderFunction/ActionFunction return type to prevent undefined from being a valid return value (#10267)fetcher.load call to a route without a loader (#10345)createRouter detectErrorBoundary option in favor of the new mapRouteProperties option for converting a framework-agnostic route to a framework-aware route. This allows us to set more than just the hasErrorBoundary property during route pre-processing, and is now used for mapping Component -> element and ErrorBoundary -> errorElement in react-router. (#10287)loader revalidations). However, since fetchers have a static href, they should only revalidate on action submissions or router.revalidate calls. (#10344)AbortController usage between revalidating fetchers and the thing that triggered them such that the unmount/deletion of a revalidating fetcher doesn't impact the ongoing triggering navigation/revalidation (#10271)Added support for Future Flags in React Router. The first flag being introduced is future.v7_normalizeFormMethod which will normalize the exposed useNavigation()/useFetcher() formMethod fields as uppercase HTTP methods to align with the fetch() behavior. (#10207)
future.v7_normalizeFormMethod === false (default v6 behavior),useNavigation().formMethod is lowercaseuseFetcher().formMethod is lowercasefuture.v7_normalizeFormMethod === true:useNavigation().formMethod is uppercaseuseFetcher().formMethod is uppercaseshouldRevalidate if the fetcher action redirects (#10208)lazy() errors during router initialization (#10201)instanceof check for DeferredData to be resilient to ESM/CJS boundaries in SSR bundling scenarios (#10247)@remix-run/web-fetch@4.3.3 (#10216)In order to keep your application bundles small and support code-splitting of your routes, we've introduced a new lazy() route property. This is an async function that resolves the non-route-matching portions of your route definition (loader, action, element/Component, errorElement/ErrorBoundary, shouldRevalidate, handle).
Lazy routes are resolved on initial load and during the loading or submitting phase of a navigation or fetcher call. You cannot lazily define route-matching properties (path, index, children) since we only execute your lazy route functions after we've matched known routes.
Your lazy functions will typically return the result of a dynamic import.
// In this example, we assume most folks land on the homepage so we include that
// in our critical-path bundle, but then we lazily load modules for /a and /b so
// they don't load until the user navigates to those routes
let routes = createRoutesFromElements(
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="a" lazy={() => import("./a")} />
<Route path="b" lazy={() => import("./b")} />
</Route>
);
Then in your lazy route modules, export the properties you want defined for the route:
export async function loader({ request }) {
let data = await fetchData(request);
return json(data);
}
// Export a `Component` directly instead of needing to create a React Element from it
export function Component() {
let data = useLoaderData();
return (
<>
<h1>You made it!</h1>
<p>{data}</p>
</>
);
}
// Export an `ErrorBoundary` directly instead of needing to create a React Element from it
export function ErrorBoundary() {
let error = useRouteError();
return isRouteErrorResponse(error) ? (
<h1>
{error.status} {error.statusText}
</h1>
) : (
<h1>{error.message || error}</h1>
);
}
An example of this in action can be found in the examples/lazy-loading-router-provider directory of the repository.
🙌 Huge thanks to @rossipedia for the Initial Proposal and POC Implementation.
generatePath incorrectly applying parameters in some cases (#10078)basename (#10076)defer loader responses in createStaticHandler's query() method (#10077)invariant to an UNSAFE_invariant export since it's only intended for internal use (#10066)shouldRevalidate calls (#9948)
shouldRevalidate function was only being called for explicit revalidation scenarios (after a mutation, manual useRevalidator call, or an X-Remix-Revalidate header used for cookie setting in Remix). It was not properly being called on implicit revalidation scenarios that also apply to navigation loader revalidation, such as a change in search params or clicking a link for the page we're already on. It's now correctly called in those additional scenarios.current*/next* parameters reflected the static fetcher.load URL (and thus were identical). Instead, they should have reflected the the navigation that triggered the revalidation (as the form* parameters did). These parameters now correctly reflect the triggering navigation.preventScrollReset on <fetcher.Form> (#9963)instanceof check from isRouteErrorResponse to avoid bundling issues on the server (#9930)defer call only contains critical data and remove the AbortController (#9965)File FormData entries (#9867)createStaticHandler (#9760)generatePath when optional params are present (#9764)OPTIONS requests in staticHandler.queryRoute (#9914)shouldRevalidate on action redirects (#9777, #9782)actionData on action redirect to current location (#9772)unstable_ prefix from createStaticHandler/createStaticRouter/StaticRouterProvider (#9738)replace on submissions and PUSH on submission to new paths (#9734)useLoaderData usage in errorElement (#9735)hydrationData (#9664)This release introduces support for Optional Route Segments. Now, adding a ? to the end of any path segment will make that entire segment optional. This works for both static segments and dynamic parameters.
Optional Params Examples
lang?/about will match:
/:lang/about/about/multistep/:widget1?/widget2?/widget3? will match:
/multistep/multistep/:widget1/multistep/:widget1/:widget2/multistep/:widget1/:widget2/:widget3Optional Static Segment Example
/home? will match:
//home/fr?/about will match:
/about/fr/about<Route path="prefix-:param">, to align with how splat parameters work. If you were previously relying on this behavior then it's recommended to extract the static portion of the path at the useParams call site: (#9506)// Old behavior at URL /prefix-123
<Route path="prefix-:id" element={<Comp /> }>
function Comp() {
let params = useParams(); // { id: '123' }
let id = params.id; // "123"
...
}
// New behavior at URL /prefix-123
<Route path=":id" element={<Comp /> }>
function Comp() {
let params = useParams(); // { id: 'prefix-123' }
let id = params.id.replace(/^prefix-/, ''); // "123"
...
}
headers on loader request's after SSR document action request (#9721)GET request (#9680)instanceof Response checks in favor of isResponse (#9690)URL creation in Cloudflare Pages or other non-browser-environments (#9682, #9689)requestContext support to static handler query/queryRoute (#9696)
queryRoute(path, routeId) has been changed to queryRoute(path, { routeId, requestContext })action/loader function returns undefined as revalidations need to know whether the loader has previously been executed. undefined also causes issues during SSR stringification for hydration. You should always ensure you loader/action returns a value, and you may return null if you don't wish to return anything. (#9511)basename in static data routers (#9591)ErrorResponse bodies to contain more descriptive text in internal 403/404/405 scenarioscreateHashRouter (#9409)basename and relative routing in loader/action redirects (#9447)action function (#9455)index routes with a path in useResolvedPath (#9486)@remix-run/router (#9446)createURL in local file execution in Firefox (#9464)unstable_createStaticHandler for incorporating into Remix (#9482, #9465)actionData after a successful action redirect (#9334)matchPath to avoid false positives on dash-separated segments (#9300)RouteObject/RouteProps types to surface the error in TypeScript. (#9366)initialEntries (#9288)?index for fetcher get submissions to index routes (#9312)This is the first stable release of @remix-run/router, which provides all the underlying routing and data loading/mutation logic for react-router. You should not be using this package directly unless you are authoring a routing library similar to react-router.
For an overview of the features provided by react-router, we recommend you go check out the docs, especially the feature overview and the tutorial.
For an overview of the features provided by @remix-run/router, please check out the README.