@remix-run/router
unstable_dataStrategy
API to allow for more advanced implementations (#11943)unstable_HandlerResult
to unstable_DataStrategyResult
unstable_DataStrategyResult[]
(parallel to matches
) to a key/value object of routeId => unstable_DataStrategyResult
match.shouldLoad
)unstable_dataStrategy
fetcherKey
parameter to unstable_dataStrategy
to allow differentiation from navigational and fetcher callshandlerOverride
instead of returning a DataStrategyResult
match.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)
We also now leverage an internal FIFO queue of previous paths we've already called unstable_patchRouteOnMiss
against so that we don't re-call on subsequent navigations to the same path
Rename 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 HandlerResult
status
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 codeshouldRevalidate
shouldRevalidate
's unstable_actionStatus
parameter to actionStatus
unstable_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)
RFC: https://github.com/remix-run/react-router/discussions/11113
unstable_patchRoutesOnMiss
docs: https://reactrouter.com/en/main/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 shouldRevalidate
shouldRevalidate
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)
Please note that 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
.
jsx
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:
jsx
<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>
:
```jsx function Dashboard() { return (
<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 "."
:
```jsx // If we are on URL /dashboard/team, and we want to link to /dashboard/team: function DashboardTeam() { // ❌ This is broken and results in return A broken link to the Current URL;
// ✅ 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:
jsx
<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:
jsx
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:
```jsx
function Dashboard() { return (
<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)getPathContributingMatches
UNSAFE_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)```jsx
function Component() {
return (
<>
{/ This is now correctly relative to /a/b, not /a/b/c /}
);
}
```
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)
This is sort of a long-standing bug fix as the 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
state
Keep an eye out for the following specific behavioral changes when opting into this flag and check your app for compatibility:
useFetchers()
. 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)
Once a fetcher has been totally unmounted, we can ignore post-processing of a persisted fetcher result such as a redirect or an error
The router will also pass a new deletedFetchers
array to the subscriber callbacks so that the UI layer can remove associated fetcher data
Add 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)```js // 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] } ```
``js
// 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" } } ```
``js
// 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)
Example: router.navigate("../path", { fromRouteId: "some-route" })
.
This also applies to router.fetch
which already receives a source route ID
Introduce a new @remix-run/router
future.v7_prependBasename
flag to enable basename
prefixing to all paths coming into router.navigate
and router.fetch
.
Previously the 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 paths
basename
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)
When 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.
jsx
// 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:
```jsx 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) ? (
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/:widget3
Optional 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)```jsx
// Old behavior at URL /prefix-123
function Comp() { let params = useParams(); // { id: '123' } let id = params.id; // "123" ... }
// New behavior at URL /prefix-123
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
.